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.

669 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. class AndroidStudioProjectExporter : public AndroidProjectExporterBase
  18. {
  19. public:
  20. //==============================================================================
  21. static const char* getName() { return "Android Studio"; }
  22. static const char* getValueTreeTypeName() { return "ANDROIDSTUDIO"; }
  23. static AndroidStudioProjectExporter* createForSettings (Project& project, const ValueTree& settings)
  24. {
  25. if (settings.hasType (getValueTreeTypeName()))
  26. return new AndroidStudioProjectExporter (project, settings);
  27. return nullptr;
  28. }
  29. //==============================================================================
  30. AndroidStudioProjectExporter (Project& p, const ValueTree& t)
  31. : AndroidProjectExporterBase (p, t),
  32. androidStudioExecutable (findAndroidStudioExecutable())
  33. {
  34. name = getName();
  35. if (getTargetLocationString().isEmpty())
  36. getTargetLocationValue() = getDefaultBuildsRootFolder() + "AndroidStudio";
  37. }
  38. //==============================================================================
  39. bool canLaunchProject() override
  40. {
  41. return androidStudioExecutable.exists();
  42. }
  43. bool launchProject() override
  44. {
  45. if (! androidStudioExecutable.exists())
  46. {
  47. jassertfalse;
  48. return false;
  49. }
  50. const File targetFolder (getTargetFolder());
  51. #if JUCE_WINDOWS
  52. // on Windows, we have to surround the path with extra quotes, otherwise Android Studio
  53. // will choke if there are any space characters in the path.
  54. return androidStudioExecutable.startAsProcess ("\"" + targetFolder.getFullPathName() + "\"");
  55. #else
  56. return androidStudioExecutable.startAsProcess(targetFolder.getFullPathName());
  57. #endif
  58. }
  59. void createExporterProperties (PropertyListBuilder& props) override
  60. {
  61. AndroidProjectExporterBase::createExporterProperties (props);
  62. props.add (new TextPropertyComponent (getNDKPlatformVersionValue(), "NDK Platform Version", 32, false),
  63. "The value to use for android$user.ndk.platformVersion in Gradle");
  64. props.add (new TextPropertyComponent (getBuildToolsVersionValue(), "Build Tools Version", 32, false),
  65. "The version of build tools use for build tools in Gradle");
  66. }
  67. Value getNDKPlatformVersionValue() { return getSetting (Ids::androidNdkPlatformVersion); }
  68. String getNDKPlatformVersionString() const { return settings [Ids::androidNdkPlatformVersion]; }
  69. Value getBuildToolsVersionValue() { return getSetting (Ids::buildToolsVersion); }
  70. String getBuildToolsVersionString() const { return settings [Ids::buildToolsVersion]; }
  71. void removeOldFiles (const File& targetFolder) const
  72. {
  73. targetFolder.getChildFile ("app/src").deleteRecursively();
  74. targetFolder.getChildFile ("app/build").deleteRecursively();
  75. targetFolder.getChildFile ("app/build.gradle").deleteFile();
  76. targetFolder.getChildFile ("gradle").deleteRecursively();
  77. targetFolder.getChildFile ("local.properties").deleteFile();
  78. targetFolder.getChildFile ("settings.gradle").deleteFile();
  79. }
  80. void create (const OwnedArray<LibraryModule>& modules) const override
  81. {
  82. const File targetFolder (getTargetFolder());
  83. removeOldFiles (targetFolder);
  84. {
  85. const String package (getActivityClassPackage());
  86. const String path (package.replaceCharacter ('.', File::separator));
  87. const File javaTarget (targetFolder.getChildFile ("app/src/main/java").getChildFile (path));
  88. copyActivityJavaFiles (modules, javaTarget, package);
  89. }
  90. writeSettingsDotGradle (targetFolder);
  91. writeLocalDotProperties (targetFolder);
  92. writeBuildDotGradleRoot (targetFolder);
  93. writeBuildDotGradleApp (targetFolder);
  94. writeGradleWrapperProperties (targetFolder);
  95. writeAndroidManifest (targetFolder);
  96. writeStringsXML (targetFolder);
  97. writeAppIcons (targetFolder);
  98. createSourceSymlinks (targetFolder);
  99. }
  100. static File findAndroidStudioExecutable()
  101. {
  102. #if JUCE_WINDOWS
  103. const File defaultInstallation ("C:\\Program Files\\Android\\Android Studio\\bin");
  104. if (defaultInstallation.exists())
  105. {
  106. {
  107. const File studio64 = defaultInstallation.getChildFile ("studio64.exe");
  108. if (studio64.existsAsFile())
  109. return studio64;
  110. }
  111. {
  112. const File studio = defaultInstallation.getChildFile ("studio.exe");
  113. if (studio.existsAsFile())
  114. return studio;
  115. }
  116. }
  117. #elif JUCE_MAC
  118. const File defaultInstallation ("/Applications/Android Studio.app");
  119. if (defaultInstallation.exists())
  120. return defaultInstallation;
  121. #endif
  122. return File::nonexistent;
  123. }
  124. protected:
  125. //==============================================================================
  126. class AndroidStudioBuildConfiguration : public BuildConfiguration
  127. {
  128. public:
  129. AndroidStudioBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e)
  130. : BuildConfiguration (p, settings, e)
  131. {
  132. if (getArchitectures().isEmpty())
  133. {
  134. if (isDebug())
  135. getArchitecturesValue() = "armeabi x86";
  136. else
  137. getArchitecturesValue() = "armeabi armeabi-v7a x86";
  138. }
  139. }
  140. Value getArchitecturesValue() { return getValue (Ids::androidArchitectures); }
  141. String getArchitectures() const { return config [Ids::androidArchitectures]; }
  142. var getDefaultOptimisationLevel() const override { return var ((int) (isDebug() ? gccO0 : gccO3)); }
  143. void createConfigProperties (PropertyListBuilder& props) override
  144. {
  145. addGCCOptimisationProperty (props);
  146. props.add (new TextPropertyComponent (getArchitecturesValue(), "Architectures", 256, false),
  147. "A list of the ARM architectures to build (for a fat binary).");
  148. }
  149. };
  150. BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override
  151. {
  152. return new AndroidStudioBuildConfiguration (project, v, *this);
  153. }
  154. private:
  155. static void createSymboicLinkAndCreateParentFolders (const File& originalFile, const File& linkFile)
  156. {
  157. {
  158. const File linkFileParentDirectory (linkFile.getParentDirectory());
  159. // this will recursively creative the parent directories for the file
  160. // without this, the symlink would fail because it doesn't automatically create
  161. // the folders if they don't exist
  162. if (! linkFileParentDirectory.createDirectory())
  163. throw SaveError (String ("Could not create directory ") + linkFileParentDirectory.getFullPathName());
  164. }
  165. if (! originalFile.createSymbolicLink (linkFile, true))
  166. throw SaveError (String ("Failed to create symlink from ")
  167. + linkFile.getFullPathName() + " to "
  168. + originalFile.getFullPathName() + "!");
  169. }
  170. void makeSymlinksForGroup (const Project::Item& group, const File& targetFolder) const
  171. {
  172. if (! group.isGroup())
  173. {
  174. throw SaveError ("makeSymlinksForGroup was called with something other than a group!");
  175. }
  176. for (int i = 0; i < group.getNumChildren(); ++i)
  177. {
  178. const Project::Item& projectItem = group.getChild (i);
  179. if (projectItem.isGroup())
  180. {
  181. makeSymlinksForGroup (projectItem, targetFolder.getChildFile (projectItem.getName()));
  182. }
  183. else if (projectItem.shouldBeAddedToTargetProject()) // must be a file then
  184. {
  185. const File originalFile (projectItem.getFile());
  186. const File targetFile (targetFolder.getChildFile (originalFile.getFileName()));
  187. createSymboicLinkAndCreateParentFolders (originalFile, targetFile);
  188. }
  189. }
  190. }
  191. void createSourceSymlinks (const File& folder) const
  192. {
  193. const File targetFolder (folder.getChildFile ("app/src/main/jni"));
  194. // here we make symlinks to only to files included in the groups inside the project
  195. // this is because Android Studio does not have a concept of groups and just uses
  196. // the file system layout to determine what's to be compiled
  197. {
  198. const Array<Project::Item>& groups = getAllGroups();
  199. for (int i = 0; i < groups.size(); ++i)
  200. {
  201. const Project::Item projectItem (groups.getReference (i));
  202. const String projectItemName (projectItem.getName());
  203. if (projectItem.isGroup())
  204. makeSymlinksForGroup (projectItem, projectItemName == "Juce Modules" ? targetFolder.getChildFile ("JuceModules") : targetFolder);
  205. }
  206. }
  207. }
  208. void writeAppIcons (const File& folder) const
  209. {
  210. writeIcons (folder.getChildFile ("app/src/main/res/"));
  211. }
  212. void writeSettingsDotGradle (const File& folder) const
  213. {
  214. MemoryOutputStream memoryOutputStream;
  215. memoryOutputStream << "include ':app'";
  216. overwriteFileIfDifferentOrThrow (folder.getChildFile ("settings.gradle"), memoryOutputStream);
  217. }
  218. static String sanitisePath (String path)
  219. {
  220. return expandHomeFolderToken (path).replace ("\\", "\\\\");
  221. }
  222. static String expandHomeFolderToken (const String& path)
  223. {
  224. String homeFolder = File::getSpecialLocation (File::userHomeDirectory).getFullPathName();
  225. return path.replace ("${user.home}", homeFolder)
  226. .replace ("~", homeFolder);
  227. }
  228. void writeLocalDotProperties (const File& folder) const
  229. {
  230. MemoryOutputStream memoryOutputStream;
  231. memoryOutputStream << "ndk.dir=" << sanitisePath (getNDKPathString()) << newLine
  232. << "sdk.dir=" << sanitisePath (getSDKPathString());
  233. overwriteFileIfDifferentOrThrow (folder.getChildFile ("local.properties"), memoryOutputStream);
  234. }
  235. void writeGradleWrapperProperties (const File& folder) const
  236. {
  237. MemoryOutputStream memoryOutputStream;
  238. memoryOutputStream << "distributionUrl=https\\://services.gradle.org/distributions/gradle-2.6-all.zip";
  239. overwriteFileIfDifferentOrThrow (folder.getChildFile ("gradle/wrapper/gradle-wrapper.properties"), memoryOutputStream);
  240. }
  241. void writeBuildDotGradleRoot (const File& folder) const
  242. {
  243. MemoryOutputStream memoryOutputStream;
  244. const String indent = getIndentationString();
  245. // this is needed to make sure the correct version of
  246. // the gradle build tools is available
  247. // otherwise, the user will get an error about
  248. // com.android.tools.something not being available
  249. memoryOutputStream << "buildscript {" << newLine
  250. << indent << "repositories {" << newLine
  251. << indent << indent << "jcenter()" << newLine
  252. << indent << "}" << newLine
  253. << indent << "dependencies {" << newLine
  254. << indent << indent << "classpath 'com.android.tools.build:gradle-experimental:0.3.0-alpha7'" << newLine
  255. << indent << "}" << newLine
  256. << "}" << newLine
  257. << newLine
  258. << "allprojects {" << newLine
  259. << indent << "repositories {" << newLine
  260. << indent << indent << "jcenter()" << newLine
  261. << indent << "}" << newLine
  262. << "}";
  263. overwriteFileIfDifferentOrThrow (folder.getChildFile ("build.gradle"), memoryOutputStream);
  264. }
  265. void writeStringsXML (const File& folder) const
  266. {
  267. XmlElement strings ("resources");
  268. XmlElement* resourceName = strings.createNewChildElement ("string");
  269. resourceName->setAttribute ("name", "app_name");
  270. resourceName->addTextElement (projectName);
  271. writeXmlOrThrow (strings, folder.getChildFile ("app/src/main/res/values/string.xml"), "utf-8", 100, true);
  272. }
  273. void writeAndroidManifest (const File& folder) const
  274. {
  275. ScopedPointer<XmlElement> manifest (createManifestXML());
  276. writeXmlOrThrow (*manifest, folder.getChildFile ("app/src/main/AndroidManifest.xml"), "utf-8", 100, true);
  277. }
  278. String createModelDotAndroid (const String& indent,
  279. const String& minimumSDKVersion,
  280. const String& buildToolsVersion,
  281. const String& bundleIdentifier) const
  282. {
  283. String result;
  284. result << "android {" << newLine
  285. << indent << "compileSdkVersion = " << minimumSDKVersion << newLine
  286. << indent << "buildToolsVersion = \"" << buildToolsVersion << "\"" << newLine
  287. << indent << "defaultConfig.with {" << newLine
  288. << indent << indent << "applicationId = \"" << bundleIdentifier.toLowerCase() << "\"" << newLine
  289. << indent << indent << "minSdkVersion.apiLevel = " << minimumSDKVersion << newLine
  290. << indent << indent << "targetSdkVersion.apiLevel = " << minimumSDKVersion << newLine
  291. << indent << "}" << newLine
  292. << "}" << newLine;
  293. return result;
  294. }
  295. String createModelDotCompileOptions (const String& indent) const
  296. {
  297. String result;
  298. result << "compileOptions.with {" << newLine
  299. << indent << "sourceCompatibility = JavaVersion.VERSION_1_7" << newLine
  300. << indent << indent << "targetCompatibility = JavaVersion.VERSION_1_7" << newLine
  301. << "}" << newLine;
  302. return result;
  303. }
  304. String createModelDotAndroidSources (const String& indent) const
  305. {
  306. String result;
  307. result << "android.sources {" << newLine
  308. << indent << "main {" << newLine
  309. << indent << indent << "jni {" << newLine
  310. << indent << indent << indent << "source {" << newLine
  311. << indent << indent << indent << indent << "exclude \"**/JuceModules/\"" << newLine
  312. << indent << indent << indent << "}" << newLine
  313. << indent << indent << "}" << newLine
  314. << indent << "}" << newLine
  315. << "}" << newLine;
  316. return result;
  317. }
  318. struct ShouldBeAddedToProjectPredicate
  319. {
  320. bool operator() (const Project::Item& projectItem) const { return projectItem.shouldBeAddedToTargetProject(); }
  321. };
  322. StringArray getCPPFlags() const
  323. {
  324. StringArray result;
  325. result.add ("\"-fsigned-char\"");
  326. result.add ("\"-fexceptions\"");
  327. result.add ("\"-frtti\"");
  328. if (isCPP11Enabled())
  329. result.add ("\"-std=gnu++11\"");
  330. // preprocessor definitions
  331. {
  332. StringPairArray preprocessorDefinitions = getAllPreprocessorDefs();
  333. preprocessorDefinitions.set ("JUCE_ANDROID", "1");
  334. preprocessorDefinitions.set ("JUCE_ANDROID_API_VERSION", getMinimumSDKVersionString());
  335. preprocessorDefinitions.set ("JUCE_ANDROID_ACTIVITY_CLASSNAME", getJNIActivityClassName().replaceCharacter ('/', '_'));
  336. preprocessorDefinitions.set ("JUCE_ANDROID_ACTIVITY_CLASSPATH", "\\\"" + getActivityClassPath().replaceCharacter('.', '/') + "\\\"");
  337. const StringArray& keys = preprocessorDefinitions.getAllKeys();
  338. for (int i = 0; i < keys.size(); ++i)
  339. result.add (String ("\"-D") + keys[i] + String ("=") + preprocessorDefinitions[keys[i]] + "\"");
  340. }
  341. // include paths
  342. result.add ("\"-I${project.rootDir}/app\".toString()");
  343. result.add ("\"-I${ext.juceRootDir}\".toString()");
  344. result.add ("\"-I${ext.juceModuleDir}\".toString()");
  345. {
  346. Array<RelativePath> cppFiles;
  347. const Array<Project::Item>& groups = getAllGroups();
  348. for (int i = 0; i < groups.size(); ++i)
  349. findAllProjectItemsWithPredicate (groups.getReference (i), cppFiles, ShouldBeAddedToProjectPredicate());
  350. for (int i = 0; i < cppFiles.size(); ++i)
  351. {
  352. const RelativePath absoluteSourceFile (cppFiles.getReference (i).rebased (getTargetFolder(),
  353. project.getProjectFolder(),
  354. RelativePath::projectFolder));
  355. const String absoluteIncludeFolder (sanitisePath (project.getProjectFolder().getFullPathName() + "/"
  356. + absoluteSourceFile.toUnixStyle().upToLastOccurrenceOf ("/", false, false)));
  357. result.addIfNotAlreadyThere ("\"-I" + absoluteIncludeFolder + "\".toString()");
  358. }
  359. }
  360. return result;
  361. }
  362. StringArray getLDLibs() const
  363. {
  364. StringArray result;
  365. result.add ("android");
  366. result.add ("EGL");
  367. result.add ("GLESv2");
  368. result.add ("log");
  369. result.addArray (StringArray::fromTokens(getExternalLibrariesString(), ";", ""));
  370. return result;
  371. }
  372. String createModelDotAndroidNDK (const String& indent) const
  373. {
  374. String result;
  375. const String platformVersion (getNDKPlatformVersionString());
  376. result << "android.ndk {" << newLine
  377. << indent << "moduleName = \"juce_jni\"" << newLine
  378. << indent << "stl = \"gnustl_static\"" << newLine
  379. << indent << "toolchainVersion = 4.9" << newLine;
  380. if (platformVersion.isNotEmpty())
  381. result << indent << "platformVersion = " << getNDKPlatformVersionString() << newLine;
  382. result << indent << "ext {" << newLine
  383. << indent << indent << "juceRootDir = \"" << "${project.rootDir}/../../../../" << "\".toString()" << newLine
  384. << indent << indent << "juceModuleDir = \"" << "${juceRootDir}/modules" << "\".toString()" << newLine
  385. << indent << "}" << newLine;
  386. // CPP flags
  387. {
  388. StringArray cppFlags (getCPPFlags());
  389. for (int i = 0; i < cppFlags.size(); ++i)
  390. result << indent << "cppFlags += " << cppFlags[i] << newLine;
  391. }
  392. // libraries
  393. {
  394. StringArray libraries (getLDLibs());
  395. result << indent << "ldLibs += [";
  396. for (int i = 0; i < libraries.size(); ++i)
  397. {
  398. result << "\"" << libraries[i] << "\"";
  399. if (i + 1 != libraries.size())
  400. result << ", ";
  401. }
  402. result << "]" << newLine;
  403. }
  404. result << "}" << newLine;
  405. return result;
  406. }
  407. String getGradleCPPFlags (const String& indent, const ConstConfigIterator& config) const
  408. {
  409. String result;
  410. StringArray rootFlags;
  411. StringArray ndkFlags;
  412. if (config->isDebug())
  413. {
  414. ndkFlags.add ("debuggable = true");
  415. ndkFlags.add ("cppFlags += \"-g\"");
  416. ndkFlags.add ("cppFlags += \"-DDEBUG=1\"");
  417. ndkFlags.add ("cppFlags += \"-D_DEBUG=1\"");
  418. }
  419. else
  420. {
  421. rootFlags.add ("minifyEnabled = true");
  422. rootFlags.add ("proguardFiles += 'proguard-android-optimize.txt'");
  423. ndkFlags.add ("cppFlags += \"-DNDEBUG=1\"");
  424. }
  425. {
  426. StringArray extraFlags (StringArray::fromTokens (getExtraCompilerFlagsString(), " ", ""));
  427. for (int i = 0; extraFlags.size(); ++i)
  428. ndkFlags.add (String ("cppFlags += \"") + extraFlags[i] + "\"");
  429. }
  430. // there appears to be an issue with build types that have a name other than
  431. // "debug" or "release". Apparently this is hard coded in Android Studio ...
  432. {
  433. const String configName (config->getName());
  434. if (configName != "Debug" && configName != "Release")
  435. throw SaveError ("Build configurations other than Debug and Release are not yet support for Android Studio");
  436. result << configName.toLowerCase() << " {" << newLine;
  437. }
  438. for (int i = 0; i < rootFlags.size(); ++i)
  439. result << indent << rootFlags[i] << newLine;
  440. result << indent << "ndk.with {" << newLine;
  441. for (int i = 0; i < ndkFlags.size(); ++i)
  442. result << indent << indent << ndkFlags[i] << newLine;
  443. result << indent << "}" << newLine
  444. << "}" << newLine;
  445. return result;
  446. }
  447. String createModelDotAndroidDotBuildTypes (const String& indent) const
  448. {
  449. String result;
  450. result << "android.buildTypes {" << newLine;
  451. for (ConstConfigIterator config (*this); config.next();)
  452. result << CodeHelpers::indent (getGradleCPPFlags (indent, config), indent.length(), true);
  453. result << "}";
  454. return result;
  455. }
  456. String createModelDotAndroidDotProductFlavors (const String& indent) const
  457. {
  458. String result;
  459. result << "android.productFlavors {" << newLine;
  460. // TODO! - this needs to be changed so that it generates seperate flags for debug and release ...
  461. // at present, it just generates all ABIs for all build types
  462. StringArray architectures (StringArray::fromTokens (getABIs<AndroidStudioBuildConfiguration> (true), " ", ""));
  463. architectures.mergeArray (StringArray::fromTokens (getABIs<AndroidStudioBuildConfiguration> (false), " ", ""));
  464. if (architectures.size() == 0)
  465. throw SaveError ("Can't build for no architectures!");
  466. for (int i = 0; i < architectures.size(); ++i)
  467. {
  468. String architecture (architectures[i].trim());
  469. if (architecture.isEmpty())
  470. continue;
  471. result << indent << "create(\"" << architecture << "\") {" << newLine
  472. << indent << indent << "ndk.abiFilters += \"" << architecture << "\"" << newLine
  473. << indent << "}" << newLine;
  474. }
  475. result << "}" << newLine;
  476. return result;
  477. }
  478. void writeBuildDotGradleApp (const File& folder) const
  479. {
  480. MemoryOutputStream memoryOutputStream;
  481. const String indent = getIndentationString();
  482. const String minimumSDKVersion = getMinimumSDKVersionString();
  483. const String bundleIdentifier = project.getBundleIdentifier().toString();
  484. String buildToolsVersion = getBuildToolsVersionString();
  485. if (buildToolsVersion.isEmpty())
  486. buildToolsVersion = "23.0.1";
  487. memoryOutputStream << "apply plugin: 'com.android.model.application'" << newLine
  488. << newLine
  489. << "model {" << newLine
  490. << CodeHelpers::indent (createModelDotAndroid (indent,
  491. minimumSDKVersion,
  492. buildToolsVersion,
  493. bundleIdentifier), indent.length(), true)
  494. << newLine
  495. << CodeHelpers::indent (createModelDotCompileOptions (indent), indent.length(), true)
  496. << newLine
  497. << CodeHelpers::indent (createModelDotAndroidSources (indent), indent.length(), true)
  498. << newLine
  499. << CodeHelpers::indent (createModelDotAndroidNDK (indent), indent.length(), true)
  500. << newLine
  501. << CodeHelpers::indent (createModelDotAndroidDotBuildTypes (indent), indent.length(), true)
  502. << newLine
  503. << CodeHelpers::indent (createModelDotAndroidDotProductFlavors (indent), indent.length(), true)
  504. << "}";
  505. overwriteFileIfDifferentOrThrow (folder.getChildFile ("app/build.gradle"), memoryOutputStream);
  506. }
  507. static const char* getIndentationString() noexcept
  508. {
  509. return " ";
  510. }
  511. const File androidStudioExecutable;
  512. JUCE_DECLARE_NON_COPYABLE (AndroidStudioProjectExporter)
  513. };