examples: annotate examples
[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"]
382 var icon_available = false
383 for res in resolutions do
384 var path = project_root / "android/res/drawable-{res}/icon.png"
385 if path.file_exists then
386 icon_available = true
387 break
388 end
389 end
390
391 var icon_declaration
392 if icon_available then
393 icon_declaration = "android:icon=\"@drawable/icon\""
394 else icon_declaration = ""
395
396 # TODO android:roundIcon
397
398 # Copy the Java sources files
399 var java_dir = android_app_main / "java/"
400 java_dir.mkdir
401 for mmodule in compiler.mainmodule.in_importation.greaters do
402 var extra_java_files = mmodule.extra_java_files
403 if extra_java_files != null then for file in extra_java_files do
404 var path = file.filename
405 path.file_copy_to(java_dir/path.basename)
406 end
407 end
408
409 # ---
410 # /app/src/main/AndroidManifest.xml
411
412 var manifest_file = new FileWriter.open(android_app_main / "AndroidManifest.xml")
413 manifest_file.write """
414 <?xml version="1.0" encoding="utf-8"?>
415 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
416 package="{{{app_package}}}">
417
418 <application
419 android:hasCode="true"
420 android:allowBackup="true"
421 android:label="@string/app_name"
422 {{{icon_declaration}}}>
423 """
424
425 for activity in project.activities do
426 manifest_file.write """
427 <activity android:name="{{{activity}}}"
428 {{{project.manifest_activity_attributes.join("\n")}}}
429 {{{icon_declaration}}}>
430
431 <meta-data android:name="android.app.lib_name" android:value="nit_app" />
432
433 <intent-filter>
434 <action android:name="android.intent.action.MAIN" />
435 <category android:name="android.intent.category.LAUNCHER" />
436 </intent-filter>
437 </activity>
438 """
439 end
440
441 manifest_file.write """
442 {{{project.manifest_application_lines.join("\n")}}}
443
444 </application>
445
446 {{{project.manifest_lines.join("\n")}}}
447
448 </manifest>
449 """
450 manifest_file.close
451 end
452
453 redef fun write_makefile(compile_dir, cfiles)
454 do
455 # Do nothing, already done in `write_files`
456 end
457
458 redef fun compile_c_code(compile_dir)
459 do
460 var android_project_root = android_project_root.as(not null)
461 var release = toolcontext.opt_release.value
462
463 # Compile C and Java code into an APK file
464 var verb = if release then "assembleRelease" else "assembleDebug"
465 var args = [gradlew_dir/"gradlew", verb, "-p", android_project_root]
466 if toolcontext.opt_verbose.value <= 1 then args.add "-q"
467 toolcontext.exec_and_check(args, "Android project error")
468
469 # Move the APK to the target
470 var outname = outfile(compiler.mainmodule)
471 if release then
472 var apk_path = "{android_project_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
473
474 # Sign APK
475 var keystore_path= "KEYSTORE".environ
476 var key_alias= "KEY_ALIAS".environ
477 var tsa_server= "TSA_SERVER".environ
478
479 if key_alias.is_empty then
480 toolcontext.warning(null, "key-alias",
481 "Warning: the environment variable `KEY_ALIAS` is not set, the APK file will not be signed.")
482
483 # Just move the unsigned APK to outname
484 args = ["mv", apk_path, outname]
485 toolcontext.exec_and_check(args, "Android project error")
486 return
487 end
488
489 # We have a key_alias, try to sign the APK
490 args = ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path, key_alias]
491
492 ## Use a custom keystore
493 if not keystore_path.is_empty then args.add_all(["-keystore", keystore_path])
494
495 ## Use a TSA server
496 if not tsa_server.is_empty then args.add_all(["-tsa", tsa_server])
497
498 toolcontext.exec_and_check(args, "Android project error")
499
500 # Clean output file
501 if outname.to_path.exists then outname.to_path.delete
502
503 # Align APK
504 args = ["zipalign", "4", apk_path, outname]
505 toolcontext.exec_and_check(args, "Android project error")
506 else
507 # Move to the expected output path
508 args = ["mv", "{android_project_root}/app/build/outputs/apk/debug/app-debug.apk", outname]
509 toolcontext.exec_and_check(args, "Android project error")
510 end
511 end
512 end
513
514 redef class JavaClassTemplate
515 redef fun write_to_files(compdir)
516 do
517 var jni_path = "cpp/"
518 if compdir.has_suffix(jni_path) then
519 var path = "{compdir.substring(0, compdir.length-jni_path.length)}/java/"
520 return super(path)
521 else return super
522 end
523 end