1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Compile program for the Android platform
21 import compiler
::abstract_compiler
23 intrude import ffi
::extra_java_files
24 import android_annotations
26 redef class ToolContext
27 redef fun platform_from_name
(name
)
29 if name
== "android" then return new AndroidPlatform
37 redef fun name
do return "android"
39 redef fun supports_libgc
do return true
41 redef fun supports_libunwind
do return false
43 redef fun supports_linker_script
do return false
45 redef fun toolchain
(toolcontext
, compiler
) do return new AndroidToolchain(toolcontext
, compiler
)
48 class AndroidToolchain
49 super MakefileToolchain
51 var android_project_root
: nullable String = null
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/"
60 redef fun default_outname
do return "{super}.apk"
62 private fun share_dir
: Text
64 var nit_dir
= toolcontext
.nit_dir
or else "."
65 return (nit_dir
/"share").realpath
68 private fun gradlew_dir
: Text do return share_dir
/ "android-gradlew"
70 redef fun write_files
(compile_dir
, cfiles
)
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
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
83 var filepath
= root
.filepath
84 if filepath
!= null then
85 project_root
= filepath
93 var app_name
= project
.name
94 var app_package
= project
.namespace
95 var app_version
= project
.version
97 var app_min_api
= project
.min_api
98 if app_min_api
== null then app_min_api
= 10
100 var app_target_api
= project
.target_api
101 if app_target_api
== null then app_target_api
= app_min_api
104 if project
.max_api
!= null then app_max_api
= "maxSdkVersion {project.max_api.as(not null)}"
106 # Create basic directory structure
109 android_project_root
.mkdir
110 android_app_root
.mkdir
111 (android_app_root
/"libs").mkdir
113 var android_app_main
= android_app_root
/ "src/main"
114 android_app_main
.mkdir
115 (android_app_main
/ "java").mkdir
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
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."
131 alpha_comparator
.sort available_versions
132 build_tools_version
= available_versions
.last
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
]
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"
152 # TODO make configurable client-side
153 var compile_sdk_version
= app_target_api
155 var local_build_gradle
= """
156 apply plugin: 'com.android.application'
158 {{{platform_version}}}
161 compileSdkVersion {{{compile_sdk_version}}}
162 buildToolsVersion "{{{build_tools_version}}}"
165 applicationId "{{{app_package}}}"
166 minSdkVersion {{{app_min_api}}}
168 targetSdkVersion {{{app_target_api}}}
169 versionCode {{{project.version_code}}}
170 versionName "{{{app_version}}}"
172 abiFilters 'armeabi-v7a', 'x86'
174 externalNativeBuild {
176 arguments "-DANDROID_TOOLCHAIN=gcc"
184 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
188 externalNativeBuild {
190 path "src/main/cpp/CMakeLists.txt"
200 implementation fileTree(dir: 'libs', include: ['*.jar'])
203 local_build_gradle
.write_to_file
"{android_project_root}/app/build.gradle"
205 # TODO add 'arm64-v8a' and 'x86_64' to `abiFilters` when the min API is available
208 # Other, smaller files
211 var global_build_gradle
= """
218 classpath 'com.android.tools.build:gradle:3.0.0'
229 global_build_gradle
.write_to_file
"{android_project_root}/build.gradle"
232 var settings_gradle
= """
235 settings_gradle
.write_to_file
"{android_project_root}/settings.gradle"
238 var gradle_properties
= """
239 org.gradle.jvmargs=-Xmx1536m
241 gradle_properties
.write_to_file
"{android_project_root}/gradle.properties"
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;"
249 # compile normal C files
253 # /app/src/main/cpp/CMakeLists.txt
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
)
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)
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"
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")
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)
289 cmake_minimum_required(VERSION 3.4.1)
291 {{{add_native_app_glue}}}
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})
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)
306 ## link_map is already defined in Android
307 add_definitions("-DGC_DONT_DEFINE_LINK_MAP")
310 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
312 add_subdirectory(${lib_src_DIR} ${lib_build_DIR} )
313 include_directories(${lib_src_DIR}/include)
318 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DANDROID -DWITH_LIBGC")
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")
325 add_library(nit_app SHARED {{{cfiles.join("\n\t")}}} )
327 target_include_directories(nit_app PRIVATE ${ANDROID_NDK}/sources/android/native_app_glue)
332 target_link_libraries(nit_app gc-lib
333 {{{target_link_libraries.join("\n\t")}}})
335 cmakelists
.write_to_file
"{android_app_main}/cpp/CMakeLists.txt"
338 # /app/src/main/res/values/strings.xml for app name
340 # Set the default pretty application name
341 var res_values_dir
= "{android_app_main}/res/values/"
343 """<?xml version="1.0" encoding="utf-8"?>
345 <string name="app_name">{{{app_name}}}</string>
346 </resources>""".write_to_file res_values_dir
/"strings.xml"
349 # Copy assets, resources in the Android project
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
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")
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")
374 # Generate AndroidManifest.xml
377 var resolutions
= ["ldpi", "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi", "anydpi", "anydpi-v26"]
379 var has_round
= false
381 for res
in resolutions
do
383 if "{project_root}/android/res/mipmap-{res}/ic_launcher_round.png".file_exists
then
386 if "{project_root}/android/res/mipmap-{res}/ic_launcher.png".file_exists
then
387 icon_name
= "@mipmap/ic_launcher"
390 if "{project_root}/android/res/mipmap-{res}/ic_launcher.xml".file_exists
then
391 icon_name
= "@mipmap/ic_launcher"
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"
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\
""
412 else icon_declaration
= ""
414 # TODO android:roundIcon
416 # Copy the Java sources files
417 var java_dir
= android_app_main
/ "java/"
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
425 path
.file_copy_to
(java_dir
/file
.filename
)
430 # /app/src/main/AndroidManifest.xml
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}}}">
439 android:hasCode="true"
440 android:allowBackup="true"
441 android:label="@string/app_name"
442 {{{icon_declaration}}}>
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}}}>
451 <meta-data android:name="android.app.lib_name" android:value="nit_app" />
454 <action android:name="android.intent.action.MAIN" />
455 <category android:name="android.intent.category.LAUNCHER" />
461 manifest_file
.write
"""
462 {{{project.manifest_application_lines.join("\n")}}}
466 {{{project.manifest_lines.join("\n")}}}
473 redef fun write_makefile
(compile_dir
, cfiles
)
475 # Do nothing, already done in `write_files`
478 redef fun compile_c_code
(compile_dir
)
480 var android_project_root
= android_project_root
.as(not null)
481 var release
= toolcontext
.opt_release
.value
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")
489 # Move the APK to the target
490 var outname
= outfile
(compiler
.mainmodule
)
492 var apk_path
= "{android_project_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
495 var keystore_path
= "KEYSTORE".environ
496 var key_alias
= "KEY_ALIAS".environ
497 var tsa_server
= "TSA_SERVER".environ
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.")
503 # Just move the unsigned APK to outname
504 args
= ["mv", apk_path
, outname
]
505 toolcontext
.exec_and_check
(args
, "Android project error")
509 # We have a key_alias, try to sign the APK
510 args
= ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path
, key_alias
]
512 ## Use a custom keystore
513 if not keystore_path
.is_empty
then args
.add_all
(["-keystore", keystore_path
])
516 if not tsa_server
.is_empty
then args
.add_all
(["-tsa", tsa_server
])
518 toolcontext
.exec_and_check
(args
, "Android project error")
521 if outname
.to_path
.exists
then outname
.to_path
.delete
524 args
= ["zipalign", "4", apk_path
, outname
]
525 toolcontext
.exec_and_check
(args
, "Android project error")
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")
534 redef class JavaClassTemplate
535 redef fun write_to_files
(compdir
)
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/"