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
.filename
423 path
.file_copy_to
(java_dir
/path
.basename
)
428 # /app/src/main/AndroidManifest.xml
430 var manifest_file
= new FileWriter.open
(android_app_main
/ "AndroidManifest.xml")
431 manifest_file
.write
"""
432 <?xml version="1.0" encoding="utf-8"?>
433 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
434 package="{{{app_package}}}">
437 android:hasCode="true"
438 android:allowBackup="true"
439 android:label="@string/app_name"
440 {{{icon_declaration}}}>
443 for activity
in project
.activities
do
444 manifest_file
.write
"""
445 <activity android:name="{{{activity}}}"
446 {{{project.manifest_activity_attributes.join("\n")}}}
447 {{{icon_declaration}}}>
449 <meta-data android:name="android.app.lib_name" android:value="nit_app" />
452 <action android:name="android.intent.action.MAIN" />
453 <category android:name="android.intent.category.LAUNCHER" />
459 manifest_file
.write
"""
460 {{{project.manifest_application_lines.join("\n")}}}
464 {{{project.manifest_lines.join("\n")}}}
471 redef fun write_makefile
(compile_dir
, cfiles
)
473 # Do nothing, already done in `write_files`
476 redef fun compile_c_code
(compile_dir
)
478 var android_project_root
= android_project_root
.as(not null)
479 var release
= toolcontext
.opt_release
.value
481 # Compile C and Java code into an APK file
482 var verb
= if release
then "assembleRelease" else "assembleDebug"
483 var args
= [gradlew_dir
/"gradlew", verb
, "-p", android_project_root
]
484 if toolcontext
.opt_verbose
.value
<= 1 then args
.add
"-q"
485 toolcontext
.exec_and_check
(args
, "Android project error")
487 # Move the APK to the target
488 var outname
= outfile
(compiler
.mainmodule
)
490 var apk_path
= "{android_project_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
493 var keystore_path
= "KEYSTORE".environ
494 var key_alias
= "KEY_ALIAS".environ
495 var tsa_server
= "TSA_SERVER".environ
497 if key_alias
.is_empty
then
498 toolcontext
.warning
(null, "key-alias",
499 "Warning: the environment variable `KEY_ALIAS` is not set, the APK file will not be signed.")
501 # Just move the unsigned APK to outname
502 args
= ["mv", apk_path
, outname
]
503 toolcontext
.exec_and_check
(args
, "Android project error")
507 # We have a key_alias, try to sign the APK
508 args
= ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path
, key_alias
]
510 ## Use a custom keystore
511 if not keystore_path
.is_empty
then args
.add_all
(["-keystore", keystore_path
])
514 if not tsa_server
.is_empty
then args
.add_all
(["-tsa", tsa_server
])
516 toolcontext
.exec_and_check
(args
, "Android project error")
519 if outname
.to_path
.exists
then outname
.to_path
.delete
522 args
= ["zipalign", "4", apk_path
, outname
]
523 toolcontext
.exec_and_check
(args
, "Android project error")
525 # Move to the expected output path
526 args
= ["mv", "{android_project_root}/app/build/outputs/apk/debug/app-debug.apk", outname
]
527 toolcontext
.exec_and_check
(args
, "Android project error")
532 redef class JavaClassTemplate
533 redef fun write_to_files
(compdir
)
535 var jni_path
= "cpp/"
536 if compdir
.has_suffix
(jni_path
) then
537 var path
= "{compdir.substring(0, compdir.length-jni_path.length)}/java/"