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.

691 lines
26KB

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