The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

593 lines
22KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2015 - ROLI Ltd.
  5. Permission is granted to use this software under the terms of either:
  6. a) the GPL v2 (or any later version)
  7. b) the Affero GPL v3
  8. Details of these licenses can be found at: www.gnu.org/licenses
  9. JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
  10. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  11. A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  12. ------------------------------------------------------------------------------
  13. To release a closed-source product which uses JUCE, commercial licenses are
  14. available: visit www.juce.com for more information.
  15. ==============================================================================
  16. */
  17. #include "../Project/jucer_Project.h"
  18. #include "../Project/jucer_Module.h"
  19. #include "jucer_CommandLine.h"
  20. //==============================================================================
  21. namespace
  22. {
  23. static const char* getLineEnding() { return "\r\n"; }
  24. struct CommandLineError
  25. {
  26. CommandLineError (const String& s) : message (s) {}
  27. String message;
  28. };
  29. static void hideDockIcon()
  30. {
  31. #if JUCE_MAC
  32. Process::setDockIconVisible (false);
  33. #endif
  34. }
  35. static bool matchArgument (const String& arg, const String& possible)
  36. {
  37. return arg == possible
  38. || arg == "-" + possible
  39. || arg == "--" + possible;
  40. }
  41. static void checkArgumentCount (const StringArray& args, int minNumArgs)
  42. {
  43. if (args.size() < minNumArgs)
  44. throw CommandLineError ("Not enough arguments!");
  45. }
  46. static File getFile (const String& filename)
  47. {
  48. return File::getCurrentWorkingDirectory().getChildFile (filename.unquoted());
  49. }
  50. static File getDirectoryCheckingForExistence (const String& filename)
  51. {
  52. File f = getFile (filename);
  53. if (! f.isDirectory())
  54. throw CommandLineError ("Could not find folder: " + f.getFullPathName());
  55. return f;
  56. }
  57. static File getFileCheckingForExistence (const String& filename)
  58. {
  59. File f = getFile (filename);
  60. if (! f.exists())
  61. throw CommandLineError ("Could not find file: " + f.getFullPathName());
  62. return f;
  63. }
  64. static Array<File> findAllSourceFiles (const File& folder)
  65. {
  66. Array<File> files;
  67. for (DirectoryIterator di (folder, true, "*.cpp;*.cxx;*.cc;*.c;*.h;*.hpp;*.hxx;*.hpp;*.mm;*.m", File::findFiles); di.next();)
  68. files.add (di.getFile());
  69. return files;
  70. }
  71. static String joinLinesIntoSourceFile (StringArray& lines)
  72. {
  73. while (lines.size() > 10 && lines [lines.size() - 1].isEmpty())
  74. lines.remove (lines.size() - 1);
  75. return lines.joinIntoString (getLineEnding()) + getLineEnding();
  76. }
  77. static void replaceFile (const File& file, const String& newText, const String& message)
  78. {
  79. std::cout << message << file.getFullPathName() << std::endl;
  80. TemporaryFile temp (file);
  81. if (! temp.getFile().replaceWithText (newText, false, false))
  82. throw CommandLineError ("!!! ERROR Couldn't write to temp file!");
  83. if (! temp.overwriteTargetFileWithTemporary())
  84. throw CommandLineError ("!!! ERROR Couldn't write to file!");
  85. }
  86. //==============================================================================
  87. struct LoadedProject
  88. {
  89. LoadedProject (const String& fileToLoad)
  90. {
  91. hideDockIcon();
  92. File projectFile = getFileCheckingForExistence (fileToLoad);
  93. if (! projectFile.hasFileExtension (Project::projectFileExtension))
  94. throw CommandLineError (projectFile.getFullPathName() + " isn't a valid jucer project file!");
  95. project = new Project (projectFile);
  96. if (! project->loadFrom (projectFile, true))
  97. {
  98. project = nullptr;
  99. throw CommandLineError ("Failed to load the project file: " + projectFile.getFullPathName());
  100. }
  101. }
  102. void save (bool justSaveResources)
  103. {
  104. if (project != nullptr)
  105. {
  106. Result error (justSaveResources ? project->saveResourcesOnly (project->getFile())
  107. : project->saveProject (project->getFile(), true));
  108. project = nullptr;
  109. if (error.failed())
  110. throw CommandLineError ("Error when saving: " + error.getErrorMessage());
  111. }
  112. }
  113. ScopedPointer<Project> project;
  114. };
  115. //==============================================================================
  116. /* Running a command-line of the form "introjucer --resave foobar.jucer" will try to load
  117. that project and re-export all of its targets.
  118. */
  119. static void resaveProject (const StringArray& args, bool justSaveResources)
  120. {
  121. checkArgumentCount (args, 2);
  122. LoadedProject proj (args[1]);
  123. std::cout << (justSaveResources ? "Re-saving project resources: "
  124. : "Re-saving file: ")
  125. << proj.project->getFile().getFullPathName() << std::endl;
  126. proj.save (justSaveResources);
  127. }
  128. //==============================================================================
  129. static void setVersion (const StringArray& args)
  130. {
  131. checkArgumentCount (args, 3);
  132. LoadedProject proj (args[2]);
  133. String version (args[1].trim());
  134. std::cout << "Setting project version: " << version << std::endl;
  135. proj.project->getVersionValue() = version;
  136. proj.save (false);
  137. }
  138. //==============================================================================
  139. static void bumpVersion (const StringArray& args)
  140. {
  141. checkArgumentCount (args, 2);
  142. LoadedProject proj (args[1]);
  143. String version = proj.project->getVersionString();
  144. version = version.upToLastOccurrenceOf (".", true, false)
  145. + String (version.getTrailingIntValue() + 1);
  146. std::cout << "Bumping project version to: " << version << std::endl;
  147. proj.project->getVersionValue() = version;
  148. proj.save (false);
  149. }
  150. static void gitTag (const StringArray& args)
  151. {
  152. checkArgumentCount (args, 2);
  153. LoadedProject proj (args[1]);
  154. String version (proj.project->getVersionValue().toString());
  155. if (version.trim().isEmpty())
  156. throw CommandLineError ("Cannot read version number from project!");
  157. StringArray command;
  158. command.add ("git");
  159. command.add ("tag");
  160. command.add ("-a");
  161. command.add (version);
  162. command.add ("-m");
  163. command.add (version.quoted());
  164. std::cout << "Performing command: " << command.joinIntoString(" ") << std::endl;
  165. ChildProcess c;
  166. if (! c.start (command, 0))
  167. throw CommandLineError ("Cannot run git!");
  168. c.waitForProcessToFinish (10000);
  169. if (c.getExitCode() != 0)
  170. throw CommandLineError ("git command failed!");
  171. }
  172. //==============================================================================
  173. static void showStatus (const StringArray& args)
  174. {
  175. hideDockIcon();
  176. checkArgumentCount (args, 2);
  177. LoadedProject proj (args[1]);
  178. std::cout << "Project file: " << proj.project->getFile().getFullPathName() << std::endl
  179. << "Name: " << proj.project->getTitle() << std::endl
  180. << "UID: " << proj.project->getProjectUID() << std::endl;
  181. EnabledModuleList& modules = proj.project->getModules();
  182. if (int numModules = modules.getNumModules())
  183. {
  184. std::cout << "Modules:" << std::endl;
  185. for (int i = 0; i < numModules; ++i)
  186. std::cout << " " << modules.getModuleID (i) << std::endl;
  187. }
  188. }
  189. //==============================================================================
  190. static String getModulePackageName (const LibraryModule& module)
  191. {
  192. return module.getID() + ".jucemodule";
  193. }
  194. static void zipModule (const File& targetFolder, const File& moduleFolder)
  195. {
  196. jassert (targetFolder.isDirectory());
  197. const File moduleFolderParent (moduleFolder.getParentDirectory());
  198. LibraryModule module (moduleFolder.getChildFile (ModuleDescription::getManifestFileName()));
  199. if (! module.isValid())
  200. throw CommandLineError (moduleFolder.getFullPathName() + " is not a valid module folder!");
  201. const File targetFile (targetFolder.getChildFile (getModulePackageName (module)));
  202. ZipFile::Builder zip;
  203. {
  204. DirectoryIterator i (moduleFolder, true, "*", File::findFiles);
  205. while (i.next())
  206. if (! i.getFile().isHidden())
  207. zip.addFile (i.getFile(), 9, i.getFile().getRelativePathFrom (moduleFolderParent));
  208. }
  209. std::cout << "Writing: " << targetFile.getFullPathName() << std::endl;
  210. TemporaryFile temp (targetFile);
  211. ScopedPointer<FileOutputStream> out (temp.getFile().createOutputStream());
  212. bool ok = out != nullptr && zip.writeToStream (*out, nullptr);
  213. out = nullptr;
  214. ok = ok && temp.overwriteTargetFileWithTemporary();
  215. if (! ok)
  216. throw CommandLineError ("Failed to write to the target file: " + targetFile.getFullPathName());
  217. }
  218. static void buildModules (const StringArray& args, const bool buildAllWithIndex)
  219. {
  220. hideDockIcon();
  221. checkArgumentCount (args, 3);
  222. const File targetFolder (getFile (args[1]));
  223. if (! targetFolder.isDirectory())
  224. throw CommandLineError ("The first argument must be the directory to put the result.");
  225. if (buildAllWithIndex)
  226. {
  227. const File folderToSearch (getFile (args[2]));
  228. DirectoryIterator i (folderToSearch, false, "*", File::findDirectories);
  229. var infoList;
  230. while (i.next())
  231. {
  232. LibraryModule module (i.getFile().getChildFile (ModuleDescription::getManifestFileName()));
  233. if (module.isValid())
  234. {
  235. zipModule (targetFolder, i.getFile());
  236. var moduleInfo (new DynamicObject());
  237. moduleInfo.getDynamicObject()->setProperty ("file", getModulePackageName (module));
  238. moduleInfo.getDynamicObject()->setProperty ("info", module.moduleInfo.moduleInfo);
  239. infoList.append (moduleInfo);
  240. }
  241. }
  242. const File indexFile (targetFolder.getChildFile ("modulelist"));
  243. std::cout << "Writing: " << indexFile.getFullPathName() << std::endl;
  244. indexFile.replaceWithText (JSON::toString (infoList), false, false);
  245. }
  246. else
  247. {
  248. for (int i = 2; i < args.size(); ++i)
  249. zipModule (targetFolder, getFile (args[i]));
  250. }
  251. }
  252. //==============================================================================
  253. struct CleanupOptions
  254. {
  255. bool removeTabs;
  256. bool fixDividerComments;
  257. };
  258. static void cleanWhitespace (const File& file, CleanupOptions options)
  259. {
  260. const String content (file.loadFileAsString());
  261. if (content.contains ("%%") && content.contains ("//["))
  262. return; // ignore introjucer GUI template files
  263. StringArray lines;
  264. lines.addLines (content);
  265. bool anyTabsRemoved = false;
  266. for (int i = 0; i < lines.size(); ++i)
  267. {
  268. String& line = lines.getReference(i);
  269. if (options.removeTabs && line.containsChar ('\t'))
  270. {
  271. anyTabsRemoved = true;
  272. for (;;)
  273. {
  274. const int tabPos = line.indexOfChar ('\t');
  275. if (tabPos < 0)
  276. break;
  277. const int spacesPerTab = 4;
  278. const int spacesNeeded = spacesPerTab - (tabPos % spacesPerTab);
  279. line = line.replaceSection (tabPos, 1, String::repeatedString (" ", spacesNeeded));
  280. }
  281. }
  282. if (options.fixDividerComments)
  283. {
  284. String afterIndent (line.trim());
  285. if (afterIndent.startsWith ("//") && afterIndent.length() > 20)
  286. {
  287. afterIndent = afterIndent.substring (2);
  288. if (afterIndent.containsOnly ("=")
  289. || afterIndent.containsOnly ("/")
  290. || afterIndent.containsOnly ("-"))
  291. {
  292. line = line.substring (0, line.indexOfChar ('/'))
  293. + "//" + String::repeatedString ("=", 78);
  294. }
  295. }
  296. }
  297. line = line.trimEnd();
  298. }
  299. if (options.removeTabs && ! anyTabsRemoved)
  300. return;
  301. const String newText = joinLinesIntoSourceFile (lines);
  302. if (newText != content && newText != content + getLineEnding())
  303. replaceFile (file, newText, options.removeTabs ? "Removing tabs in: "
  304. : "Cleaning file: ");
  305. }
  306. static void scanFilesForCleanup (const StringArray& args, CleanupOptions options)
  307. {
  308. checkArgumentCount (args, 2);
  309. const File target (getFileCheckingForExistence (args[1]));
  310. Array<File> files;
  311. if (target.isDirectory())
  312. files = findAllSourceFiles (target);
  313. else
  314. files.add (target);
  315. for (int i = 0; i < files.size(); ++i)
  316. cleanWhitespace (files.getReference(i), options);
  317. }
  318. static void cleanWhitespace (const StringArray& args, bool replaceTabs)
  319. {
  320. CleanupOptions options = { replaceTabs, false };
  321. scanFilesForCleanup (args, options);
  322. }
  323. static void tidyDividerComments (const StringArray& args)
  324. {
  325. CleanupOptions options = { false, true };
  326. scanFilesForCleanup (args, options);
  327. }
  328. //==============================================================================
  329. static File findSimilarlyNamedHeader (const Array<File>& allFiles, const String& name)
  330. {
  331. for (int i = 0; i < allFiles.size(); ++i)
  332. {
  333. const File& f = allFiles.getReference(i);
  334. if (f.getFileName().equalsIgnoreCase (name))
  335. return f;
  336. }
  337. return File();
  338. }
  339. static void fixIncludes (const File& file, const Array<File>& allFiles)
  340. {
  341. const String content (file.loadFileAsString());
  342. StringArray lines;
  343. lines.addLines (content);
  344. bool hasChanged = false;
  345. for (int i = 0; i < lines.size(); ++i)
  346. {
  347. String line = lines[i];
  348. if (line.trimStart().startsWith ("#include \""))
  349. {
  350. const String includedFile (line.fromFirstOccurrenceOf ("\"", true, false)
  351. .upToLastOccurrenceOf ("\"", true, false)
  352. .trim()
  353. .unquoted());
  354. const File target (file.getSiblingFile (includedFile));
  355. if (! target.exists())
  356. {
  357. File header = findSimilarlyNamedHeader (allFiles, target.getFileName());
  358. if (header.exists())
  359. {
  360. lines.set (i, line.upToFirstOccurrenceOf ("#include \"", true, false)
  361. + header.getRelativePathFrom (file.getParentDirectory())
  362. .replaceCharacter ('\\', '/')
  363. + "\"");
  364. hasChanged = true;
  365. }
  366. }
  367. }
  368. }
  369. if (hasChanged)
  370. {
  371. const String newText = joinLinesIntoSourceFile (lines);
  372. if (newText != content && newText != content + getLineEnding())
  373. replaceFile (file, newText, "Fixing includes in: ");
  374. }
  375. }
  376. static void fixRelativeIncludePaths (const StringArray& args)
  377. {
  378. checkArgumentCount (args, 2);
  379. const File target (getDirectoryCheckingForExistence (args[1]));
  380. Array<File> files = findAllSourceFiles (target);
  381. for (int i = 0; i < files.size(); ++i)
  382. fixIncludes (files.getReference(i), files);
  383. }
  384. //==============================================================================
  385. static void showHelp()
  386. {
  387. hideDockIcon();
  388. const String appName (JUCEApplication::getInstance()->getApplicationName());
  389. std::cout << appName << std::endl
  390. << std::endl
  391. << "Usage: " << std::endl
  392. << std::endl
  393. << " " << appName << " --resave project_file" << std::endl
  394. << " Resaves all files and resources in a project." << std::endl
  395. << std::endl
  396. << " " << appName << " --resave-resources project_file" << std::endl
  397. << " Resaves just the binary resources for a project." << std::endl
  398. << std::endl
  399. << " " << appName << " --set-version version_number project_file" << std::endl
  400. << " Updates the version number in a project." << std::endl
  401. << std::endl
  402. << " " << appName << " --bump-version project_file" << std::endl
  403. << " Updates the minor version number in a project by 1." << std::endl
  404. << std::endl
  405. << " " << appName << " --git-tag-version project_file" << std::endl
  406. << " Invokes 'git tag' to attach the project's version number to the current git repository." << std::endl
  407. << std::endl
  408. << " " << appName << " --status project_file" << std::endl
  409. << " Displays information about a project." << std::endl
  410. << std::endl
  411. << " " << appName << " --buildmodule target_folder module_folder" << std::endl
  412. << " Zips a module into a downloadable file format." << std::endl
  413. << std::endl
  414. << " " << appName << " --buildallmodules target_folder module_folder" << std::endl
  415. << " Zips all modules in a given folder and creates an index for them." << std::endl
  416. << std::endl
  417. << " " << appName << " --trim-whitespace target_folder" << std::endl
  418. << " Scans the given folder for C/C++ source files (recursively), and trims any trailing whitespace from their lines, as well as normalising their line-endings to CR-LF." << std::endl
  419. << std::endl
  420. << " " << appName << " --remove-tabs target_folder" << std::endl
  421. << " Scans the given folder for C/C++ source files (recursively), and replaces any tab characters with 4 spaces." << std::endl
  422. << std::endl
  423. << " " << appName << " --tidy-divider-comments target_folder" << std::endl
  424. << " Scans the given folder for C/C++ source files (recursively), and normalises any juce-style comment division lines (i.e. any lines that look like //===== or //------- or /////////// will be replaced)." << std::endl
  425. << std::endl
  426. << " " << appName << " --fix-broken-include-paths target_folder" << std::endl
  427. << " Scans the given folder for C/C++ source files (recursively). Where a file contains an #include of one of the other filenames, it changes it to use the optimum relative path. Helpful for auto-fixing includes when re-arranging files and folders in a project." << std::endl
  428. << std::endl;
  429. }
  430. }
  431. //==============================================================================
  432. int performCommandLine (const String& commandLine)
  433. {
  434. StringArray args;
  435. args.addTokens (commandLine, true);
  436. args.trim();
  437. String command (args[0]);
  438. try
  439. {
  440. if (matchArgument (command, "help")) { showHelp(); return 0; }
  441. if (matchArgument (command, "h")) { showHelp(); return 0; }
  442. if (matchArgument (command, "resave")) { resaveProject (args, false); return 0; }
  443. if (matchArgument (command, "resave-resources")) { resaveProject (args, true); return 0; }
  444. if (matchArgument (command, "set-version")) { setVersion (args); return 0; }
  445. if (matchArgument (command, "bump-version")) { bumpVersion (args); return 0; }
  446. if (matchArgument (command, "git-tag-version")) { gitTag (args); return 0; }
  447. if (matchArgument (command, "buildmodule")) { buildModules (args, false); return 0; }
  448. if (matchArgument (command, "buildallmodules")) { buildModules (args, true); return 0; }
  449. if (matchArgument (command, "status")) { showStatus (args); return 0; }
  450. if (matchArgument (command, "trim-whitespace")) { cleanWhitespace (args, false); return 0; }
  451. if (matchArgument (command, "remove-tabs")) { cleanWhitespace (args, true); return 0; }
  452. if (matchArgument (command, "tidy-divider-comments")) { tidyDividerComments (args); return 0; }
  453. if (matchArgument (command, "fix-broken-include-paths")) { fixRelativeIncludePaths (args); return 0; }
  454. }
  455. catch (const CommandLineError& error)
  456. {
  457. std::cout << error.message << std::endl << std::endl;
  458. return 1;
  459. }
  460. return commandLineNotPerformed;
  461. }