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