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", "anydpi", "anydpi-v26"]
383 var has_round
= false
385 for res
in resolutions
do
387 if "{project_root}/android/res/mipmap-{res}/ic_launcher_round.png".file_exists
then
390 if "{project_root}/android/res/mipmap-{res}/ic_launcher.png".file_exists
then
391 icon_name
= "@mipmap/ic_launcher"
394 if "{project_root}/android/res/mipmap-{res}/ic_launcher.xml".file_exists
then
395 icon_name
= "@mipmap/ic_launcher"
399 if icon_name
== null then
400 # Old style drawable-hdpi/icon.png
401 for res
in resolutions
do
402 var path
= project_root
/ "android/res/drawable-{res}/icon.png"
403 if path
.file_exists
then
404 icon_name
= "@drawable/icon"
411 if icon_name
!= null then
412 icon_declaration
= "android:icon=\"{icon_name}\
""
413 if app_target_api
>= 25 and has_round
then
414 icon_declaration
+= "\n\t\tandroid:roundIcon=\"@mipmap
/ic_launcher_round\
""
416 else icon_declaration
= ""
418 # TODO android:roundIcon
420 # Copy the Java sources files
421 var java_dir
= android_app_main
/ "java/"
423 for mmodule
in compiler
.mainmodule
.in_importation
.greaters
do
424 var extra_java_files
= mmodule
.extra_java_files
425 if extra_java_files
!= null then for file
in extra_java_files
do
426 var path
= file
.filename
427 path
.file_copy_to
(java_dir
/path
.basename
)
432 # /app/src/main/AndroidManifest.xml
434 var manifest_file
= new FileWriter.open
(android_app_main
/ "AndroidManifest.xml")
435 manifest_file
.write
"""
436 <?xml version="1.0" encoding="utf-8"?>
437 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
438 package="{{{app_package}}}">
441 android:hasCode="true"
442 android:allowBackup="true"
443 android:label="@string/app_name"
444 {{{icon_declaration}}}>
447 for activity
in project
.activities
do
448 manifest_file
.write
"""
449 <activity android:name="{{{activity}}}"
450 {{{project.manifest_activity_attributes.join("\n")}}}
451 {{{icon_declaration}}}>
453 <meta-data android:name="android.app.lib_name" android:value="nit_app" />
456 <action android:name="android.intent.action.MAIN" />
457 <category android:name="android.intent.category.LAUNCHER" />
463 manifest_file
.write
"""
464 {{{project.manifest_application_lines.join("\n")}}}
468 {{{project.manifest_lines.join("\n")}}}
475 redef fun write_makefile
(compile_dir
, cfiles
)
477 # Do nothing, already done in `write_files`
480 redef fun compile_c_code
(compile_dir
)
482 var android_project_root
= android_project_root
.as(not null)
483 var release
= toolcontext
.opt_release
.value
485 # Compile C and Java code into an APK file
486 var verb
= if release
then "assembleRelease" else "assembleDebug"
487 var args
= [gradlew_dir
/"gradlew", verb
, "-p", android_project_root
]
488 if toolcontext
.opt_verbose
.value
<= 1 then args
.add
"-q"
489 toolcontext
.exec_and_check
(args
, "Android project error")
491 # Move the APK to the target
492 var outname
= outfile
(compiler
.mainmodule
)
494 var apk_path
= "{android_project_root}/app/build/outputs/apk/release/app-release-unsigned.apk"
497 var keystore_path
= "KEYSTORE".environ
498 var key_alias
= "KEY_ALIAS".environ
499 var tsa_server
= "TSA_SERVER".environ
501 if key_alias
.is_empty
then
502 toolcontext
.warning
(null, "key-alias",
503 "Warning: the environment variable `KEY_ALIAS` is not set, the APK file will not be signed.")
505 # Just move the unsigned APK to outname
506 args
= ["mv", apk_path
, outname
]
507 toolcontext
.exec_and_check
(args
, "Android project error")
511 # We have a key_alias, try to sign the APK
512 args
= ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path
, key_alias
]
514 ## Use a custom keystore
515 if not keystore_path
.is_empty
then args
.add_all
(["-keystore", keystore_path
])
518 if not tsa_server
.is_empty
then args
.add_all
(["-tsa", tsa_server
])
520 toolcontext
.exec_and_check
(args
, "Android project error")
523 if outname
.to_path
.exists
then outname
.to_path
.delete
526 args
= ["zipalign", "4", apk_path
, outname
]
527 toolcontext
.exec_and_check
(args
, "Android project error")
529 # Move to the expected output path
530 args
= ["mv", "{android_project_root}/app/build/outputs/apk/debug/app-debug.apk", outname
]
531 toolcontext
.exec_and_check
(args
, "Android project error")
536 redef class JavaClassTemplate
537 redef fun write_to_files
(compdir
)
539 var jni_path
= "cpp/"
540 if compdir
.has_suffix
(jni_path
) then
541 var path
= "{compdir.substring(0, compdir.length-jni_path.length)}/java/"