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.

678 lines
25KB

  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 "../jucer_Headers.h"
  18. #include "../Project/jucer_Project.h"
  19. #include "../Project/jucer_Module.h"
  20. #include "jucer_CommandLine.h"
  21. //==============================================================================
  22. namespace
  23. {
  24. static const char* getLineEnding() { return "\r\n"; }
  25. struct CommandLineError
  26. {
  27. CommandLineError (const String& s) : message (s) {}
  28. String message;
  29. };
  30. static void hideDockIcon()
  31. {
  32. #if JUCE_MAC
  33. Process::setDockIconVisible (false);
  34. #endif
  35. }
  36. static bool matchArgument (const String& arg, const String& possible)
  37. {
  38. return arg == possible
  39. || arg == "-" + possible
  40. || arg == "--" + possible;
  41. }
  42. static void checkArgumentCount (const StringArray& args, int minNumArgs)
  43. {
  44. if (args.size() < minNumArgs)
  45. throw CommandLineError ("Not enough arguments!");
  46. }
  47. static File getFile (const String& filename)
  48. {
  49. return File::getCurrentWorkingDirectory().getChildFile (filename.unquoted());
  50. }
  51. static File getDirectoryCheckingForExistence (const String& filename)
  52. {
  53. File f = getFile (filename);
  54. if (! f.isDirectory())
  55. throw CommandLineError ("Could not find folder: " + f.getFullPathName());
  56. return f;
  57. }
  58. static File getFileCheckingForExistence (const String& filename)
  59. {
  60. File f = getFile (filename);
  61. if (! f.exists())
  62. throw CommandLineError ("Could not find file: " + f.getFullPathName());
  63. return f;
  64. }
  65. static Array<File> findAllSourceFiles (const File& folder)
  66. {
  67. Array<File> files;
  68. for (DirectoryIterator di (folder, true, "*.cpp;*.cxx;*.cc;*.c;*.h;*.hpp;*.hxx;*.hpp;*.mm;*.m", File::findFiles); di.next();)
  69. files.add (di.getFile());
  70. return files;
  71. }
  72. static String joinLinesIntoSourceFile (StringArray& lines)
  73. {
  74. while (lines.size() > 10 && lines [lines.size() - 1].isEmpty())
  75. lines.remove (lines.size() - 1);
  76. return lines.joinIntoString (getLineEnding()) + getLineEnding();
  77. }
  78. static void replaceFile (const File& file, const String& newText, const String& message)
  79. {
  80. std::cout << message << file.getFullPathName() << std::endl;
  81. TemporaryFile temp (file);
  82. if (! temp.getFile().replaceWithText (newText, false, false))
  83. throw CommandLineError ("!!! ERROR Couldn't write to temp file!");
  84. if (! temp.overwriteTargetFileWithTemporary())
  85. throw CommandLineError ("!!! ERROR Couldn't write to file!");
  86. }
  87. //==============================================================================
  88. struct LoadedProject
  89. {
  90. LoadedProject (const String& fileToLoad)
  91. {
  92. hideDockIcon();
  93. File projectFile = getFileCheckingForExistence (fileToLoad);
  94. if (! projectFile.hasFileExtension (Project::projectFileExtension))
  95. throw CommandLineError (projectFile.getFullPathName() + " isn't a valid jucer project file!");
  96. project = new Project (projectFile);
  97. if (! project->loadFrom (projectFile, true))
  98. {
  99. project = nullptr;
  100. throw CommandLineError ("Failed to load the project file: " + projectFile.getFullPathName());
  101. }
  102. }
  103. void save (bool justSaveResources)
  104. {
  105. if (project != nullptr)
  106. {
  107. Result error (justSaveResources ? project->saveResourcesOnly (project->getFile())
  108. : project->saveProject (project->getFile(), true));
  109. project = nullptr;
  110. if (error.failed())
  111. throw CommandLineError ("Error when saving: " + error.getErrorMessage());
  112. }
  113. }
  114. ScopedPointer<Project> project;
  115. };
  116. //==============================================================================
  117. /* Running a command-line of the form "introjucer --resave foobar.jucer" will try to load
  118. that project and re-export all of its targets.
  119. */
  120. static void resaveProject (const StringArray& args, bool justSaveResources)
  121. {
  122. checkArgumentCount (args, 2);
  123. LoadedProject proj (args[1]);
  124. std::cout << (justSaveResources ? "Re-saving project resources: "
  125. : "Re-saving file: ")
  126. << proj.project->getFile().getFullPathName() << std::endl;
  127. proj.save (justSaveResources);
  128. }
  129. //==============================================================================
  130. static void setVersion (const StringArray& args)
  131. {
  132. checkArgumentCount (args, 3);
  133. LoadedProject proj (args[2]);
  134. String version (args[1].trim());
  135. std::cout << "Setting project version: " << version << std::endl;
  136. proj.project->getVersionValue() = version;
  137. proj.save (false);
  138. }
  139. //==============================================================================
  140. static void bumpVersion (const StringArray& args)
  141. {
  142. checkArgumentCount (args, 2);
  143. LoadedProject proj (args[1]);
  144. String version = proj.project->getVersionString();
  145. version = version.upToLastOccurrenceOf (".", true, false)
  146. + String (version.getTrailingIntValue() + 1);
  147. std::cout << "Bumping project version to: " << version << std::endl;
  148. proj.project->getVersionValue() = version;
  149. proj.save (false);
  150. }
  151. static void gitTag (const StringArray& args)
  152. {
  153. checkArgumentCount (args, 2);
  154. LoadedProject proj (args[1]);
  155. String version (proj.project->getVersionValue().toString());
  156. if (version.trim().isEmpty())
  157. throw CommandLineError ("Cannot read version number from project!");
  158. StringArray command;
  159. command.add ("git");
  160. command.add ("tag");
  161. command.add ("-a");
  162. command.add (version);
  163. command.add ("-m");
  164. command.add (version.quoted());
  165. std::cout << "Performing command: " << command.joinIntoString(" ") << std::endl;
  166. ChildProcess c;
  167. if (! c.start (command, 0))
  168. throw CommandLineError ("Cannot run git!");
  169. c.waitForProcessToFinish (10000);
  170. if (c.getExitCode() != 0)
  171. throw CommandLineError ("git command failed!");
  172. }
  173. //==============================================================================
  174. static void showStatus (const StringArray& args)
  175. {
  176. hideDockIcon();
  177. checkArgumentCount (args, 2);
  178. LoadedProject proj (args[1]);
  179. std::cout << "Project file: " << proj.project->getFile().getFullPathName() << std::endl
  180. << "Name: " << proj.project->getTitle() << std::endl
  181. << "UID: " << proj.project->getProjectUID() << std::endl;
  182. EnabledModuleList& modules = proj.project->getModules();
  183. if (int numModules = modules.getNumModules())
  184. {
  185. std::cout << "Modules:" << std::endl;
  186. for (int i = 0; i < numModules; ++i)
  187. std::cout << " " << modules.getModuleID (i) << std::endl;
  188. }
  189. }
  190. //==============================================================================
  191. static String getModulePackageName (const LibraryModule& module)
  192. {
  193. return module.getID() + ".jucemodule";
  194. }
  195. static void zipModule (const File& targetFolder, const File& moduleFolder)
  196. {
  197. jassert (targetFolder.isDirectory());
  198. const File moduleFolderParent (moduleFolder.getParentDirectory());
  199. LibraryModule module (moduleFolder.getChildFile (ModuleDescription::getManifestFileName()));
  200. if (! module.isValid())
  201. throw CommandLineError (moduleFolder.getFullPathName() + " is not a valid module folder!");
  202. const File targetFile (targetFolder.getChildFile (getModulePackageName (module)));
  203. ZipFile::Builder zip;
  204. {
  205. DirectoryIterator i (moduleFolder, true, "*", File::findFiles);
  206. while (i.next())
  207. if (! i.getFile().isHidden())
  208. zip.addFile (i.getFile(), 9, i.getFile().getRelativePathFrom (moduleFolderParent));
  209. }
  210. std::cout << "Writing: " << targetFile.getFullPathName() << std::endl;
  211. TemporaryFile temp (targetFile);
  212. ScopedPointer<FileOutputStream> out (temp.getFile().createOutputStream());
  213. bool ok = out != nullptr && zip.writeToStream (*out, nullptr);
  214. out = nullptr;
  215. ok = ok && temp.overwriteTargetFileWithTemporary();
  216. if (! ok)
  217. throw CommandLineError ("Failed to write to the target file: " + targetFile.getFullPathName());
  218. }
  219. static void buildModules (const StringArray& args, const bool buildAllWithIndex)
  220. {
  221. hideDockIcon();
  222. checkArgumentCount (args, 3);
  223. const File targetFolder (getFile (args[1]));
  224. if (! targetFolder.isDirectory())
  225. throw CommandLineError ("The first argument must be the directory to put the result.");
  226. if (buildAllWithIndex)
  227. {
  228. const File folderToSearch (getFile (args[2]));
  229. DirectoryIterator i (folderToSearch, false, "*", File::findDirectories);
  230. var infoList;
  231. while (i.next())
  232. {
  233. LibraryModule module (i.getFile().getChildFile (ModuleDescription::getManifestFileName()));
  234. if (module.isValid())
  235. {
  236. zipModule (targetFolder, i.getFile());
  237. var moduleInfo (new DynamicObject());
  238. moduleInfo.getDynamicObject()->setProperty ("file", getModulePackageName (module));
  239. moduleInfo.getDynamicObject()->setProperty ("info", module.moduleInfo.moduleInfo);
  240. infoList.append (moduleInfo);
  241. }
  242. }
  243. const File indexFile (targetFolder.getChildFile ("modulelist"));
  244. std::cout << "Writing: " << indexFile.getFullPathName() << std::endl;
  245. indexFile.replaceWithText (JSON::toString (infoList), false, false);
  246. }
  247. else
  248. {
  249. for (int i = 2; i < args.size(); ++i)
  250. zipModule (targetFolder, getFile (args[i]));
  251. }
  252. }
  253. //==============================================================================
  254. struct CleanupOptions
  255. {
  256. bool removeTabs;
  257. bool fixDividerComments;
  258. };
  259. static void cleanWhitespace (const File& file, CleanupOptions options)
  260. {
  261. const String content (file.loadFileAsString());
  262. if (content.contains ("%%") && content.contains ("//["))
  263. return; // ignore introjucer GUI template files
  264. StringArray lines;
  265. lines.addLines (content);
  266. bool anyTabsRemoved = false;
  267. for (int i = 0; i < lines.size(); ++i)
  268. {
  269. String& line = lines.getReference(i);
  270. if (options.removeTabs && line.containsChar ('\t'))
  271. {
  272. anyTabsRemoved = true;
  273. for (;;)
  274. {
  275. const int tabPos = line.indexOfChar ('\t');
  276. if (tabPos < 0)
  277. break;
  278. const int spacesPerTab = 4;
  279. const int spacesNeeded = spacesPerTab - (tabPos % spacesPerTab);
  280. line = line.replaceSection (tabPos, 1, String::repeatedString (" ", spacesNeeded));
  281. }
  282. }
  283. if (options.fixDividerComments)
  284. {
  285. String afterIndent (line.trim());
  286. if (afterIndent.startsWith ("//") && afterIndent.length() > 20)
  287. {
  288. afterIndent = afterIndent.substring (2);
  289. if (afterIndent.containsOnly ("=")
  290. || afterIndent.containsOnly ("/")
  291. || afterIndent.containsOnly ("-"))
  292. {
  293. line = line.substring (0, line.indexOfChar ('/'))
  294. + "//" + String::repeatedString ("=", 78);
  295. }
  296. }
  297. }
  298. line = line.trimEnd();
  299. }
  300. if (options.removeTabs && ! anyTabsRemoved)
  301. return;
  302. const String newText = joinLinesIntoSourceFile (lines);
  303. if (newText != content && newText != content + getLineEnding())
  304. replaceFile (file, newText, options.removeTabs ? "Removing tabs in: "
  305. : "Cleaning file: ");
  306. }
  307. static void scanFilesForCleanup (const StringArray& args, CleanupOptions options)
  308. {
  309. checkArgumentCount (args, 2);
  310. const File target (getFileCheckingForExistence (args[1]));
  311. Array<File> files;
  312. if (target.isDirectory())
  313. files = findAllSourceFiles (target);
  314. else
  315. files.add (target);
  316. for (int i = 0; i < files.size(); ++i)
  317. cleanWhitespace (files.getReference(i), options);
  318. }
  319. static void cleanWhitespace (const StringArray& args, bool replaceTabs)
  320. {
  321. CleanupOptions options = { replaceTabs, false };
  322. scanFilesForCleanup (args, options);
  323. }
  324. static void tidyDividerComments (const StringArray& args)
  325. {
  326. CleanupOptions options = { false, true };
  327. scanFilesForCleanup (args, options);
  328. }
  329. //==============================================================================
  330. static File findSimilarlyNamedHeader (const Array<File>& allFiles, const String& name, const File& sourceFile)
  331. {
  332. File result;
  333. for (int i = 0; i < allFiles.size(); ++i)
  334. {
  335. const File& f = allFiles.getReference(i);
  336. if (f.getFileName().equalsIgnoreCase (name) && f != sourceFile)
  337. {
  338. if (result.exists())
  339. return File(); // multiple possible results, so don't change it!
  340. result = f;
  341. }
  342. }
  343. return result;
  344. }
  345. static void fixIncludes (const File& file, const Array<File>& allFiles)
  346. {
  347. const String content (file.loadFileAsString());
  348. StringArray lines;
  349. lines.addLines (content);
  350. bool hasChanged = false;
  351. for (int i = 0; i < lines.size(); ++i)
  352. {
  353. String line = lines[i];
  354. if (line.trimStart().startsWith ("#include \""))
  355. {
  356. const String includedFile (line.fromFirstOccurrenceOf ("\"", true, false)
  357. .upToLastOccurrenceOf ("\"", true, false)
  358. .trim()
  359. .unquoted());
  360. const File target (file.getSiblingFile (includedFile));
  361. if (! target.exists())
  362. {
  363. File header = findSimilarlyNamedHeader (allFiles, target.getFileName(), file);
  364. if (header.exists())
  365. {
  366. lines.set (i, line.upToFirstOccurrenceOf ("#include \"", true, false)
  367. + header.getRelativePathFrom (file.getParentDirectory())
  368. .replaceCharacter ('\\', '/')
  369. + "\"");
  370. hasChanged = true;
  371. }
  372. }
  373. }
  374. }
  375. if (hasChanged)
  376. {
  377. const String newText = joinLinesIntoSourceFile (lines);
  378. if (newText != content && newText != content + getLineEnding())
  379. replaceFile (file, newText, "Fixing includes in: ");
  380. }
  381. }
  382. static void fixRelativeIncludePaths (const StringArray& args)
  383. {
  384. checkArgumentCount (args, 2);
  385. const File target (getDirectoryCheckingForExistence (args[1]));
  386. Array<File> files = findAllSourceFiles (target);
  387. for (int i = 0; i < files.size(); ++i)
  388. fixIncludes (files.getReference(i), files);
  389. }
  390. //==============================================================================
  391. static String getStringConcatenationExpression (Random& rng, int start, int length)
  392. {
  393. jassert (length > 0);
  394. if (length == 1)
  395. return "s" + String (start);
  396. int breakPos = jlimit (1, length - 1, (length / 3) + rng.nextInt (length / 3));
  397. return "(" + getStringConcatenationExpression (rng, start, breakPos)
  398. + " + " + getStringConcatenationExpression (rng, start + breakPos, length - breakPos) + ")";
  399. }
  400. static void generateObfuscatedStringCode (const StringArray& args)
  401. {
  402. checkArgumentCount (args, 2);
  403. const String originalText (args[1]);
  404. struct Section
  405. {
  406. String text;
  407. int position, index;
  408. void writeGenerator (MemoryOutputStream& out) const
  409. {
  410. String name ("s" + String (index));
  411. out << " String " << name << "; " << name;
  412. for (int i = 0; i < text.length(); ++i)
  413. out << " << '" << String::charToString (text[i]) << "'";
  414. out << ";" << newLine;
  415. }
  416. };
  417. Array<Section> sections;
  418. String text = originalText;
  419. Random rng;
  420. while (text.isNotEmpty())
  421. {
  422. int pos = jmax (0, text.length() - (1 + rng.nextInt (6)));
  423. Section s = { text.substring (pos), pos, 0 };
  424. sections.insert (0, s);
  425. text = text.substring (0, pos);
  426. }
  427. for (int i = 0; i < sections.size(); ++i)
  428. sections.getReference(i).index = i;
  429. for (int i = 0; i < sections.size(); ++i)
  430. sections.swap (i, rng.nextInt (sections.size()));
  431. MemoryOutputStream out;
  432. out << "String createString()" << newLine
  433. << "{" << newLine;
  434. for (int i = 0; i < sections.size(); ++i)
  435. sections.getReference(i).writeGenerator (out);
  436. out << newLine
  437. << " String result = " << getStringConcatenationExpression (rng, 0, sections.size()) << ";" << newLine
  438. << newLine
  439. << " jassert (result == " << originalText.quoted() << ");" << newLine
  440. << " return result;" << newLine
  441. << "}" << newLine;
  442. std::cout << out.toString() << std::endl;
  443. }
  444. //==============================================================================
  445. static void showHelp()
  446. {
  447. hideDockIcon();
  448. const String appName (JUCEApplication::getInstance()->getApplicationName());
  449. std::cout << appName << std::endl
  450. << std::endl
  451. << "Usage: " << std::endl
  452. << std::endl
  453. << " " << appName << " --resave project_file" << std::endl
  454. << " Resaves all files and resources in a project." << std::endl
  455. << std::endl
  456. << " " << appName << " --resave-resources project_file" << std::endl
  457. << " Resaves just the binary resources for a project." << std::endl
  458. << std::endl
  459. << " " << appName << " --set-version version_number project_file" << std::endl
  460. << " Updates the version number in a project." << std::endl
  461. << std::endl
  462. << " " << appName << " --bump-version project_file" << std::endl
  463. << " Updates the minor version number in a project by 1." << std::endl
  464. << std::endl
  465. << " " << appName << " --git-tag-version project_file" << std::endl
  466. << " Invokes 'git tag' to attach the project's version number to the current git repository." << std::endl
  467. << std::endl
  468. << " " << appName << " --status project_file" << std::endl
  469. << " Displays information about a project." << std::endl
  470. << std::endl
  471. << " " << appName << " --buildmodule target_folder module_folder" << std::endl
  472. << " Zips a module into a downloadable file format." << std::endl
  473. << std::endl
  474. << " " << appName << " --buildallmodules target_folder module_folder" << std::endl
  475. << " Zips all modules in a given folder and creates an index for them." << std::endl
  476. << std::endl
  477. << " " << appName << " --trim-whitespace target_folder" << std::endl
  478. << " 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
  479. << std::endl
  480. << " " << appName << " --remove-tabs target_folder" << std::endl
  481. << " Scans the given folder for C/C++ source files (recursively), and replaces any tab characters with 4 spaces." << std::endl
  482. << std::endl
  483. << " " << appName << " --tidy-divider-comments target_folder" << std::endl
  484. << " 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
  485. << std::endl
  486. << " " << appName << " --fix-broken-include-paths target_folder" << std::endl
  487. << " 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
  488. << std::endl
  489. << " " << appName << " --obfuscated-string-code string_to_obfuscate" << std::endl
  490. << " Generates a C++ function which returns the given string, but in an obfuscated way." << std::endl
  491. << std::endl;
  492. }
  493. }
  494. //==============================================================================
  495. int performCommandLine (const String& commandLine)
  496. {
  497. StringArray args;
  498. args.addTokens (commandLine, true);
  499. args.trim();
  500. String command (args[0]);
  501. try
  502. {
  503. if (matchArgument (command, "help")) { showHelp(); return 0; }
  504. if (matchArgument (command, "h")) { showHelp(); return 0; }
  505. if (matchArgument (command, "resave")) { resaveProject (args, false); return 0; }
  506. if (matchArgument (command, "resave-resources")) { resaveProject (args, true); return 0; }
  507. if (matchArgument (command, "set-version")) { setVersion (args); return 0; }
  508. if (matchArgument (command, "bump-version")) { bumpVersion (args); return 0; }
  509. if (matchArgument (command, "git-tag-version")) { gitTag (args); return 0; }
  510. if (matchArgument (command, "buildmodule")) { buildModules (args, false); return 0; }
  511. if (matchArgument (command, "buildallmodules")) { buildModules (args, true); return 0; }
  512. if (matchArgument (command, "status")) { showStatus (args); return 0; }
  513. if (matchArgument (command, "trim-whitespace")) { cleanWhitespace (args, false); return 0; }
  514. if (matchArgument (command, "remove-tabs")) { cleanWhitespace (args, true); return 0; }
  515. if (matchArgument (command, "tidy-divider-comments")) { tidyDividerComments (args); return 0; }
  516. if (matchArgument (command, "fix-broken-include-paths")) { fixRelativeIncludePaths (args); return 0; }
  517. if (matchArgument (command, "obfuscated-string-code")) { generateObfuscatedStringCode (args); return 0; }
  518. }
  519. catch (const CommandLineError& error)
  520. {
  521. std::cout << error.message << std::endl << std::endl;
  522. return 1;
  523. }
  524. return commandLineNotPerformed;
  525. }