7109e09b7a24ca58840790af56861f43ce4fd6f3
[nit.git] / src / platform / android.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Compile program for the Android platform
18 module android
19
20 import platform
21 import compiler::abstract_compiler
22 import ffi
23 intrude import ffi::extra_java_files
24 import android_annotations
25
26 redef class ToolContext
27 redef fun platform_from_name(name)
28 do
29 if name == "android" then return new AndroidPlatform
30 return super
31 end
32 end
33
34 class AndroidPlatform
35 super Platform
36
37 redef fun name do return "android"
38
39 redef fun supports_libgc do return true
40
41 redef fun supports_libunwind do return false
42
43 redef fun supports_linker_script do return false
44
45 redef fun toolchain(toolcontext, compiler) do return new AndroidToolchain(toolcontext, compiler)
46 end
47
48 class AndroidToolchain
49 super MakefileToolchain
50
51 var android_project_root: nullable String = null
52
53 redef fun compile_dir
54 do
55 var android_project_root = "{root_compile_dir}/android/"
56 self.android_project_root = android_project_root
57 return "{android_project_root}/app/src/main/cpp/"
58 end
59
60 redef fun default_outname do return "{super}.apk"
61
62 private fun share_dir: Text
63 do
64 var nit_dir = toolcontext.nit_dir or else "."
65 return (nit_dir/"share").realpath
66 end
67
68 private fun gradlew_dir: Text do return share_dir / "android-gradlew"
69
70 redef fun write_files(compile_dir, cfiles)
71 do
72 var android_project_root = android_project_root.as(not null)
73 var android_app_root = android_project_root/"app"
74 var project = new AndroidProject(toolcontext.modelbuilder, compiler.mainmodule)
75 var release = toolcontext.opt_release.value
76
77 # Compute the root of the project where could be assets and resources
78 var project_root = "."
79 var mpackage = compiler.mainmodule.first_real_mmodule.mpackage
80 if mpackage != null then
81 var root = mpackage.root
82 if root != null then
83 var filepath = root.filepath
84 if filepath != null then
85 project_root = filepath
86 end
87 end
88 end
89
90 # Gather app configs
91 # ---
92
93 var app_name = project.name
94 if not release then app_name += " Debug"
95
96 var app_package = project.namespace
97 if not release then app_package += "_debug"
98
99 var app_version = project.version
100
101 var app_min_api = project.min_api
102 if app_min_api == null then app_min_api = 10
103
104 var app_target_api = project.target_api
105 if app_target_api == null then app_target_api = app_min_api
106
107 var app_max_api = ""
108 if project.max_api != null then app_max_api = "maxSdkVersion {project.max_api.as(not null)}"
109
110 # Create basic directory structure
111 # ---
112
113 android_project_root.mkdir
114 android_app_root.mkdir
115 (android_app_root/"libs").mkdir
116
117 var android_app_main = android_app_root / "src/main"
118 android_app_main.mkdir
119 (android_app_main / "java").mkdir
120
121 # /app/build.gradle
122 # ---
123
124 # Use the most recent build_tools_version
125 var android_home = "ANDROID_HOME".environ
126 if android_home.is_empty then android_home = "HOME".environ / "Android/Sdk"
127 var build_tools_dir = android_home / "build-tools"
128 var available_versions = build_tools_dir.files
129
130 var build_tools_version
131 if available_versions.is_empty then
132 print_error "Error: found no Android build-tools, install one or set ANDROID_HOME."
133 return
134 else
135 alpha_comparator.sort available_versions
136 build_tools_version = available_versions.last
137 end
138
139 # Gather ldflags for Android
140 var ldflags = new Array[String]
141 var platform_name = "android"
142 for mmodule in compiler.mainmodule.in_importation.greaters do
143 if mmodule.ldflags.keys.has(platform_name) then
144 ldflags.add_all mmodule.ldflags[platform_name]
145 end
146 end
147
148 # Platform version for OpenGL ES
149 var platform_version = ""
150 if ldflags.has("-lGLESv3") then
151 platform_version = "def platformVersion = 18"
152 else if ldflags.has("-lGLESv2") then
153 platform_version = "def platformVersion = 12"
154 end
155
156 # TODO make configurable client-side
157 var compile_sdk_version = app_target_api
158
159 var local_build_gradle = """
160 apply plugin: 'com.android.application'
161
162 {{{platform_version}}}
163
164 android {
165 compileSdkVersion {{{compile_sdk_version}}}
166 buildToolsVersion "{{{build_tools_version}}}"
167
168 defaultConfig {
169 applicationId "{{{app_package}}}"
170 minSdkVersion {{{app_min_api}}}
171 {{{app_max_api}}}
172 targetSdkVersion {{{app_target_api}}}
173 versionCode {{{project.version_code}}}
174 versionName "{{{app_version}}}"
175 ndk {
176 abiFilters 'armeabi', 'armeabi-v7a', 'x86'
177 }
178 externalNativeBuild {
179 cmake {
180 cppFlags ""
181 }
182 }
183 }
184
185 buildTypes {
186 release {
187 minifyEnabled false
188 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
189 }
190 }
191
192 externalNativeBuild {
193 cmake {
194 path "src/main/cpp/CMakeLists.txt"
195 }
196 }
197
198 lintOptions {
199 abortOnError false
200 }
201 }
202
203 dependencies {
204 implementation fileTree(dir: 'libs', include: ['*.jar'])
205 }
206 """
207 local_build_gradle.write_to_file "{android_project_root}/app/build.gradle"
208
209 # TODO add 'arm64-v8a' and 'x86_64' to `abiFilters` when the min API is available
210
211 # ---
212 # Other, smaller files
213
214 # /build.gradle
215 var global_build_gradle = """
216 buildscript {
217 repositories {
218 google()
219 jcenter()
220 }
221 dependencies {
222 classpath 'com.android.tools.build:gradle:3.0.0'
223 }
224 }
225
226 allprojects {
227 repositories {
228 google()
229 jcenter()
230 }
231 }
232 """
233 global_build_gradle.write_to_file "{android_project_root}/build.gradle"
234
235 # /settings.gradle
236 var settings_gradle = """
237 include ':app'
238 """
239 settings_gradle.write_to_file "{android_project_root}/settings.gradle"
240
241 # /gradle.properties
242 var gradle_properties = """
243 org.gradle.jvmargs=-Xmx1536m
244 """
245 gradle_properties.write_to_file "{android_project_root}/gradle.properties"
246
247 # Insert an importation of the generated R class to all Java files from the FFI
248 for mod in compiler.mainmodule.in_importation.greaters do
249 var java_ffi_file = mod.java_file
250 if java_ffi_file != null then java_ffi_file.add "import {app_package}.R;"
251 end
252
253 # compile normal C files
254 super
255
256 # ---
257 # /app/src/main/cpp/CMakeLists.txt
258
259 # Gather extra C files generated elsewhere than in super
260 for f in compiler.extern_bodies do
261 if f isa ExternCFile then cfiles.add(f.filename.basename)
262 end
263
264 # Prepare for the CMakeLists format
265 var target_link_libraries = new Array[String]
266 for flag in ldflags do
267 if flag.has_prefix("-l") then
268 target_link_libraries.add flag.substring_from(2)
269 end
270 end
271
272 # Download the libgc/bdwgc sources
273 var share_dir = share_dir
274 if not share_dir.file_exists then
275 print "Android project error: Nit share directory not found, please use the environment variable NIT_DIR"
276 exit 1
277 end
278
279 var bdwgc_dir = "{share_dir}/android-bdwgc/bdwgc"
280 if not bdwgc_dir.file_exists then
281 toolcontext.exec_and_check(["{share_dir}/android-bdwgc/setup.sh"], "Android project error")
282 end
283
284 # Compile the native app glue lib if used
285 var add_native_app_glue = ""
286 if target_link_libraries.has("native_app_glue") then
287 add_native_app_glue = """
288 add_library(native_app_glue STATIC ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)
289 """
290 end
291
292 var cmakelists = """
293 cmake_minimum_required(VERSION 3.4.1)
294
295 {{{add_native_app_glue}}}
296
297
298 # libgc/bdwgc
299
300 ## The source is in the Nit repo
301 set(lib_src_DIR {{{bdwgc_dir}}})
302 set(lib_build_DIR ../libgc/outputs)
303 file(MAKE_DIRECTORY ${lib_build_DIR})
304
305 ## Config
306 add_definitions("-DGC_PTHREADS")
307 set(enable_threads TRUE)
308 set(CMAKE_USE_PTHREADS_INIT TRUE)
309
310 ## link_map is already defined in Android
311 add_definitions("-DGC_DONT_DEFINE_LINK_MAP")
312
313 ## Silence warning
314 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-tautological-pointer-compare")
315
316 add_subdirectory(${lib_src_DIR} ${lib_build_DIR} )
317 include_directories(${lib_src_DIR}/include)
318
319
320 # Nit generated code
321
322 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DANDROID -DWITH_LIBGC")
323
324 # Export ANativeActivity_onCreate(),
325 # Refer to: https://github.com/android-ndk/ndk/issues/381.
326 set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
327
328 # name so source
329 add_library(nit_app SHARED {{{cfiles.join("\n\t")}}} )
330
331 target_include_directories(nit_app PRIVATE ${ANDROID_NDK}/sources/android/native_app_glue)
332
333
334 # Link!
335
336 target_link_libraries(nit_app gc-lib
337 {{{target_link_libraries.join("\n\t")}}})
338 """
339 cmakelists.write_to_file "{android_app_main}/cpp/CMakeLists.txt"
340
341 # ---
342 # /app/src/main/res/values/strings.xml for app name
343
344 # Set the default pretty application name
345 var res_values_dir = "{android_app_main}/res/values/"
346 res_values_dir.mkdir
347 """<?xml version="1.0" encoding="utf-8"?>
348 <resources>
349 <string name="app_name">{{{app_name}}}</string>
350 </resources>""".write_to_file res_values_dir/"strings.xml"
351
352 # ---
353 # Copy assets, resources in the Android project
354
355 ## Collect path to all possible folder where we can find the `android` folder
356 var app_files = [project_root]
357 app_files.add_all project.files
358
359 for path in app_files do
360 # Copy the assets folder
361 var assets_dir = path / "assets"
362 if assets_dir.file_exists then
363 assets_dir = assets_dir.realpath
364 toolcontext.exec_and_check(["cp", "-r", assets_dir, android_app_main], "Android project error")
365 end
366
367 # Copy the whole `android` folder
368 var android_dir = path / "android"
369 if android_dir.file_exists then
370 android_dir = android_dir.realpath
371 for f in android_dir.files do
372 toolcontext.exec_and_check(["cp", "-r", android_dir / f, android_app_main], "Android project error")
373 end
374 end
375 end
376
377 # ---
378 # Generate AndroidManifest.xml
379
380 # Is there an icon?
381 var resolutions = ["ldpi", "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi", "anydpi", "anydpi-v26"]
382 var icon_name = null
383 var has_round = false
384
385 for res in resolutions do
386 # New style mipmap
387 if "{project_root}/android/res/mipmap-{res}/ic_launcher_round.png".file_exists then
388 has_round = true
389 end
390 if "{project_root}/android/res/mipmap-{res}/ic_launcher.png".file_exists then
391 icon_name = "@mipmap/ic_launcher"
392 break
393 end
394 if "{project_root}/android/res/mipmap-{res}/ic_launcher.xml".file_exists then
395 icon_name = "@mipmap/ic_launcher"
396 break
397 end
398 end
399 if icon_name == null then
400 # Old style drawable-hdpi/icon.png
401 for res in resolutions do
402 var path = project_root / "android/res/drawable-{res}/icon.png"
403 if path.file_exists then
404 icon_name = "@drawable/icon"
405 break
406 end
407 end
408 end
409
410 var icon_declaration
411 if icon_name != null then
412 icon_declaration = "android:icon=\"{icon_name}\""
413 if app_target_api >= 25 and has_round then
414 icon_declaration += "\n\t\tandroid:roundIcon=\"@mipmap/ic_launcher_round\""
415 end
416 else icon_declaration = ""
417
418 # TODO android:roundIcon
419
420 # Copy the Java sources files
421 var java_dir = android_app_main / "java/"
422 java_dir.mkdir
423 for mmodule in compiler.mainmodule.in_importation.greaters do
424 var extra_java_files = mmodule.extra_java_files
425 if extra_java_files != null then for file in extra_java_files do
426 var path = file.filename
427 path.file_copy_to(java_dir/path.basename)
428 end
429 end
430
431 # ---
432 # /app/src/main/AndroidManifest.xml
433
434 var manifest_file = new FileWriter.open(android_app_main / "AndroidManifest.xml")
435 manifest_file.write """
436 <?xml version="1.0" encoding="utf-8"?>
437 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
438 package="{{{app_package}}}">
439
440 <application
441 android:hasCode="true"
442 android:allowBackup="true"
443 android:label="@string/app_name"
444 {{{icon_declaration}}}>
445 """
446
447 for activity in project.activities do
448 manifest_file.write """
449 <activity android:name="{{{activity}}}"
450 {{{project.manifest_activity_attributes.join("\n")}}}
451 {{{icon_declaration}}}>
452
453 <meta-data android:name="android.app.lib_name" android:value="nit_app" />
454
455 <intent-filter>
456 <action android:name="android.intent.action.MAIN" />
457 <category android:name="android.intent.category.LAUNCHER" />
458 </intent-filter>
459 </activity>
460 """
461 end
462
463 manifest_file.write """
464 {{{project.manifest_application_lines.join("\n")}}}
465
466 </application>
467
468 {{{project.manifest_lines.join("\n")}}}
469
470 </manifest>
471 """
472 manifest_file.close
473 end
474
475 redef fun write_makefile(compile_dir, cfiles)
476 do
477 # Do nothing, already done in `write_files`
478 end
479
480 redef fun compile_c_code(compile_dir)
481 do
482 var android_project_root = android_project_root.as(not null)
483 var release = toolcontext.opt_release.value
484
485 # Compile C and Java code into an APK file
486 var verb = if release then "assembleRelease" else "assembleDebug"
487 var args = [gradlew_dir/"gradlew", verb, "-p", android_project_root]
488 if toolcontext.opt_verbose.value <= 1 then args.add "-q"
489 toolcontext.exec_and_check(args, "Android project error")
490
491 # Move the APK to the target
492 var outname = outfile(compiler.mainmodule)
493 if release then
494 var apk_path = "{android_project_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
495
496 # Sign APK
497 var keystore_path= "KEYSTORE".environ
498 var key_alias= "KEY_ALIAS".environ
499 var tsa_server= "TSA_SERVER".environ
500
501 if key_alias.is_empty then
502 toolcontext.warning(null, "key-alias",
503 "Warning: the environment variable `KEY_ALIAS` is not set, the APK file will not be signed.")
504
505 # Just move the unsigned APK to outname
506 args = ["mv", apk_path, outname]
507 toolcontext.exec_and_check(args, "Android project error")
508 return
509 end
510
511 # We have a key_alias, try to sign the APK
512 args = ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path, key_alias]
513
514 ## Use a custom keystore
515 if not keystore_path.is_empty then args.add_all(["-keystore", keystore_path])
516
517 ## Use a TSA server
518 if not tsa_server.is_empty then args.add_all(["-tsa", tsa_server])
519
520 toolcontext.exec_and_check(args, "Android project error")
521
522 # Clean output file
523 if outname.to_path.exists then outname.to_path.delete
524
525 # Align APK
526 args = ["zipalign", "4", apk_path, outname]
527 toolcontext.exec_and_check(args, "Android project error")
528 else
529 # Move to the expected output path
530 args = ["mv", "{android_project_root}/app/build/outputs/apk/debug/app-debug.apk", outname]
531 toolcontext.exec_and_check(args, "Android project error")
532 end
533 end
534 end
535
536 redef class JavaClassTemplate
537 redef fun write_to_files(compdir)
538 do
539 var jni_path = "cpp/"
540 if compdir.has_suffix(jni_path) then
541 var path = "{compdir.substring(0, compdir.length-jni_path.length)}/java/"
542 return super(path)
543 else return super
544 end
545 end