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.

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