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 if not release
then app_name
+= " Debug"
96 var app_package
= project
.namespace
97 if not release
then app_package
+= "_debug"
99 var app_version
= project
.version
101 var app_min_api
= project
.min_api
102 if app_min_api
== null then app_min_api
= 10
104 var app_target_api
= project
.target_api
105 if app_target_api
== null then app_target_api
= app_min_api
108 if project
.max_api
!= null then app_max_api
= "maxSdkVersion {project.max_api.as(not null)}"
110 # Create basic directory structure
113 android_project_root
.mkdir
114 android_app_root
.mkdir
115 (android_app_root
/"libs").mkdir
117 var android_app_main
= android_app_root
/ "src/main"
118 android_app_main
.mkdir
119 (android_app_main
/ "java").mkdir
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
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."
135 alpha_comparator
.sort available_versions
136 build_tools_version
= available_versions
.last
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
]
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"
156 # TODO make configurable client-side
157 var compile_sdk_version
= app_target_api
159 var local_build_gradle
= """
160 apply plugin: 'com.android.application'
162 {{{platform_version}}}
165 compileSdkVersion {{{compile_sdk_version}}}
166 buildToolsVersion "{{{build_tools_version}}}"
169 applicationId "{{{app_package}}}"
170 minSdkVersion {{{app_min_api}}}
172 targetSdkVersion {{{app_target_api}}}
173 versionCode {{{project.version_code}}}
174 versionName "{{{app_version}}}"
176 abiFilters 'armeabi', 'armeabi-v7a', 'x86'
178 externalNativeBuild {
188 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
192 externalNativeBuild {
194 path "src/main/cpp/CMakeLists.txt"
204 implementation fileTree(dir: 'libs', include: ['*.jar'])
207 local_build_gradle
.write_to_file
"{android_project_root}/app/build.gradle"
209 # TODO add 'arm64-v8a' and 'x86_64' to `abiFilters` when the min API is available
212 # Other, smaller files
215 var global_build_gradle
= """
222 classpath 'com.android.tools.build:gradle:3.0.0'
233 global_build_gradle
.write_to_file
"{android_project_root}/build.gradle"
236 var settings_gradle
= """
239 settings_gradle
.write_to_file
"{android_project_root}/settings.gradle"
242 var gradle_properties
= """
243 org.gradle.jvmargs=-Xmx1536m
245 gradle_properties
.write_to_file
"{android_project_root}/gradle.properties"
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;"
253 # compile normal C files
257 # /app/src/main/cpp/CMakeLists.txt
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
)
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)
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"
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")
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)
293 cmake_minimum_required(VERSION 3.4.1)
295 {{{add_native_app_glue}}}
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})
306 add_definitions("-DGC_PTHREADS")
307 set(enable_threads TRUE)
308 set(CMAKE_USE_PTHREADS_INIT TRUE)
310 ## link_map is already defined in Android
311 add_definitions("-DGC_DONT_DEFINE_LINK_MAP")
314 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-tautological-pointer-compare")
316 add_subdirectory(${lib_src_DIR} ${lib_build_DIR} )
317 include_directories(${lib_src_DIR}/include)
322 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DANDROID -DWITH_LIBGC")
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")
329 add_library(nit_app SHARED {{{cfiles.join("\n\t")}}} )
331 target_include_directories(nit_app PRIVATE ${ANDROID_NDK}/sources/android/native_app_glue)
336 target_link_libraries(nit_app gc-lib
337 {{{target_link_libraries.join("\n\t")}}})
339 cmakelists
.write_to_file
"{android_app_main}/cpp/CMakeLists.txt"
342 # /app/src/main/res/values/strings.xml for app name
344 # Set the default pretty application name
345 var res_values_dir
= "{android_app_main}/res/values/"
347 """<?xml version="1.0" encoding="utf-8"?>
349 <string name="app_name">{{{app_name}}}</string>
350 </resources>""".write_to_file res_values_dir
/"strings.xml"
353 # Copy assets, resources in the Android project
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
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")
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")
378 # Generate AndroidManifest.xml
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
392 if icon_available
then
393 icon_declaration
= "android:icon=\"@drawable
/icon\
""
394 else icon_declaration
= ""
396 # TODO android:roundIcon
398 # Copy the Java sources files
399 var java_dir
= android_app_main
/ "java/"
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
)
410 # /app/src/main/AndroidManifest.xml
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}}}">
419 android:hasCode="true"
420 android:allowBackup="true"
421 android:label="@string/app_name"
422 {{{icon_declaration}}}>
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}}}>
431 <meta-data android:name="android.app.lib_name" android:value="nit_app" />
434 <action android:name="android.intent.action.MAIN" />
435 <category android:name="android.intent.category.LAUNCHER" />
441 manifest_file
.write
"""
442 {{{project.manifest_application_lines.join("\n")}}}
446 {{{project.manifest_lines.join("\n")}}}
453 redef fun write_makefile
(compile_dir
, cfiles
)
455 # Do nothing, already done in `write_files`
458 redef fun compile_c_code
(compile_dir
)
460 var android_project_root
= android_project_root
.as(not null)
461 var release
= toolcontext
.opt_release
.value
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")
469 # Move the APK to the target
470 var outname
= outfile
(compiler
.mainmodule
)
472 var apk_path
= "{android_project_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
475 var keystore_path
= "KEYSTORE".environ
476 var key_alias
= "KEY_ALIAS".environ
477 var tsa_server
= "TSA_SERVER".environ
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.")
483 # Just move the unsigned APK to outname
484 args
= ["mv", apk_path
, outname
]
485 toolcontext
.exec_and_check
(args
, "Android project error")
489 # We have a key_alias, try to sign the APK
490 args
= ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path
, key_alias
]
492 ## Use a custom keystore
493 if not keystore_path
.is_empty
then args
.add_all
(["-keystore", keystore_path
])
496 if not tsa_server
.is_empty
then args
.add_all
(["-tsa", tsa_server
])
498 toolcontext
.exec_and_check
(args
, "Android project error")
501 if outname
.to_path
.exists
then outname
.to_path
.delete
504 args
= ["zipalign", "4", apk_path
, outname
]
505 toolcontext
.exec_and_check
(args
, "Android project error")
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")
514 redef class JavaClassTemplate
515 redef fun write_to_files
(compdir
)
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/"