nitc & lib: intro the @android annotation and use it in nitc
[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) do return new AndroidToolchain(toolcontext)
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 = "{super}/android/"
56 self.android_project_root = android_project_root
57 return "{android_project_root}/jni/nit_compile/"
58 end
59
60 redef fun default_outname(mainmodule) do return "{mainmodule.name}.apk"
61
62 redef fun write_files(compiler, compile_dir, cfiles)
63 do
64 var android_project_root = android_project_root.as(not null)
65 var project = toolcontext.modelbuilder.android_project_for(compiler.mainmodule)
66 var short_project_name = compiler.mainmodule.name
67 var release = toolcontext.opt_release.value
68
69 var app_name = project.name
70 if app_name == null then app_name = compiler.mainmodule.name
71
72 var app_package = project.java_package
73 if app_package == null then app_package = "org.nitlanguage.{short_project_name}"
74
75 var app_version = project.version
76 if app_version == null then app_version = "1.0"
77
78 var app_min_api = project.min_api
79 if app_min_api == null then app_min_api = 10
80
81 var app_target_api = project.target_api
82 if app_target_api == null then app_target_api = app_min_api
83
84 var app_max_api = ""
85 if project.max_api != null then app_max_api = "android:maxSdkVersion=\"{project.max_api.as(not null)}\""
86
87 # Clear the previous android project, so there is no "existing project warning"
88 # or conflict between Java files of different projects
89 if android_project_root.file_exists then android_project_root.rmdir
90
91 var args = ["android", "-s",
92 "create", "project",
93 "--name", short_project_name,
94 "--target", "android-{app_target_api}",
95 "--path", android_project_root,
96 "--package", app_package,
97 "--activity", short_project_name]
98 toolcontext.exec_and_check(args, "Android project error")
99
100 # create compile_dir
101 var dir = "{android_project_root}/jni/"
102 if not dir.file_exists then dir.mkdir
103
104 dir = compile_dir
105 if not dir.file_exists then dir.mkdir
106
107 # compile normal C files
108 super(compiler, compile_dir, cfiles)
109
110 # Gather extra C files generated elsewhere than in super
111 for f in compiler.extern_bodies do
112 if f isa ExternCFile then cfiles.add(f.filename.basename(""))
113 end
114
115 # Is there an icon?
116 var resolutions = ["ldpi", "mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"]
117 var icon_available = false
118 for res in resolutions do
119 var path = "res/drawable-{res}/icon.png"
120 if path.file_exists then
121 icon_available = true
122 break
123 end
124 end
125
126 var icon_declaration
127 if icon_available then
128 icon_declaration = "android:icon=\"@drawable/icon\""
129 else icon_declaration = ""
130
131 # Also copy over the java files
132 dir = "{android_project_root}/src/"
133 for mmodule in compiler.mainmodule.in_importation.greaters do
134 var extra_java_files = mmodule.extra_java_files
135 if extra_java_files != null then for file in extra_java_files do
136 var path = file.filename
137 path.file_copy_to(dir/path.basename(""))
138 end
139 end
140
141 ## Generate delegating makefile
142 dir = "{android_project_root}/jni/"
143 """
144 include $(call all-subdir-makefiles)
145 """.write_to_file("{dir}/Android.mk")
146
147 # Gather ldflags for Android
148 var ldflags = new Array[String]
149 var platform_name = "android"
150 for mmodule in compiler.mainmodule.in_importation.greaters do
151 if mmodule.ldflags.keys.has(platform_name) then
152 ldflags.add_all mmodule.ldflags[platform_name]
153 end
154 end
155
156 ### generate makefile into "{compile_dir}/Android.mk"
157 dir = compile_dir
158 """
159 LOCAL_PATH := $(call my-dir)
160 include $(CLEAR_VARS)
161
162 LOCAL_CFLAGS := -D ANDROID -D WITH_LIBGC
163 LOCAL_MODULE := main
164 LOCAL_SRC_FILES := \\
165 {{{cfiles.join(" \\\n")}}}
166 LOCAL_LDLIBS := {{{ldflags.join(" ")}}} libgc.a
167 LOCAL_STATIC_LIBRARIES := android_native_app_glue png
168
169 include $(BUILD_SHARED_LIBRARY)
170
171 $(call import-module,android/native_app_glue)
172 """.write_to_file("{dir}/Android.mk")
173
174 ### generate AndroidManifest.xml
175 dir = android_project_root
176 """<?xml version="1.0" encoding="utf-8"?>
177 <!-- BEGIN_INCLUDE(manifest) -->
178 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
179 package="{{{app_package}}}"
180 android:versionCode="{{{project.version_code}}}"
181 android:versionName="{{{app_version}}}">
182
183 <!-- This is the platform API where NativeActivity was introduced. -->
184 <uses-sdk
185 android:minSdkVersion="{{{app_min_api}}}"
186 android:targetSdkVersion="{{{app_target_api}}}"
187 {{{app_max_api}}} />
188
189 <application
190 android:label="@string/app_name"
191 android:hasCode="true"
192 android:debuggable="{{{not release}}}"
193 {{{icon_declaration}}}>
194
195 <!-- Our activity is the built-in NativeActivity framework class.
196 This will take care of integrating with our NDK code. -->
197 <activity android:name="android.app.NativeActivity"
198 android:label="@string/app_name"
199 {{{project.manifest_activity_attributes.join("\n")}}}
200 {{{icon_declaration}}}>
201 <!-- Tell NativeActivity the name of our .so -->
202 <meta-data android:name=\"android.app.lib_name\"
203 android:value=\"main\" />
204 <intent-filter>
205 <action android:name="android.intent.action.MAIN" />
206 <category android:name="android.intent.category.LAUNCHER" />
207 </intent-filter>
208 </activity>
209
210 {{{project.manifest_application_lines.join("\n")}}}
211
212 </application>
213
214 {{{project.manifest_lines.join("\n")}}}
215
216 </manifest>
217 <!-- END_INCLUDE(manifest) -->
218 """.write_to_file("{dir}/AndroidManifest.xml")
219
220 ### Link to png sources
221 # libpng is not available on Android NDK
222 # FIXME make obtionnal when we have alternatives to mnit
223 var nit_dir = toolcontext.nit_dir
224 var share_dir = nit_dir/"share/"
225 if not share_dir.file_exists then
226 print "Android project error: Nit share directory not found, please use the environment variable NIT_DIR"
227 exit 1
228 end
229 share_dir = share_dir.realpath
230 var target_png_dir = "{android_project_root}/jni/png"
231 if not target_png_dir.file_exists then
232 toolcontext.exec_and_check(["ln", "-s", "{share_dir}/png/", target_png_dir], "Android project error")
233 end
234
235 # Ensure that android-setup-libgc.sh has been executed
236 if not "{share_dir}/libgc/lib".file_exists then
237 toolcontext.exec_and_check(["{share_dir}/libgc/android-setup-libgc.sh"], "Android project error")
238 end
239
240 # Copy GC files
241 toolcontext.exec_and_check(["cp", "{share_dir}/libgc/lib/libgc.a", "{android_project_root}/libgc.a"], "Android project error")
242 toolcontext.exec_and_check(["ln", "-s", "{share_dir}/libgc/include/gc/", "{android_project_root}/jni/nit_compile/gc"], "Android project error")
243
244 ### Link to assets (for mnit and others)
245 # This will be accessed from `android_project_root`
246 var assets_dir
247 if compiler.mainmodule.location.file != null then
248 # it is a real file, use "{file}/../assets"
249 assets_dir = "{compiler.mainmodule.location.file.filename.dirname}/../assets"
250 else
251 # probably used -m, use "."
252 assets_dir = "assets"
253 end
254 if assets_dir.file_exists then
255 assets_dir = assets_dir.realpath
256 var target_assets_dir = "{android_project_root}/assets"
257 if not target_assets_dir.file_exists then
258 toolcontext.exec_and_check(["ln", "-s", assets_dir, target_assets_dir], "Android project error")
259 end
260 end
261
262 ### Copy resources and libs where expected by the SDK
263 var project_root
264 if compiler.mainmodule.location.file != null then
265 # it is a real file, use "{file}/../res"
266 project_root = "{compiler.mainmodule.location.file.filename.dirname}/.."
267 else
268 # probably used -m, use "."
269 project_root = "."
270 end
271
272 # Android resources folder
273 var res_dir = project_root / "res"
274 if res_dir.file_exists then
275 # copy the res folder to .nit_compile
276 res_dir = res_dir.realpath
277 toolcontext.exec_and_check(["cp", "-R", res_dir, android_project_root], "Android project error")
278 end
279
280 if not res_dir.file_exists or not "{res_dir}/values/strings.xml".file_exists then
281 # Create our own custom `res/values/string.xml` with the App name
282 """<?xml version="1.0" encoding="utf-8"?>
283 <resources>
284 <string name="app_name">{{{app_name}}}</string>
285 </resources>""".write_to_file "{dir}/res/values/strings.xml"
286 end
287
288 # Android libs folder
289 var libs_dir = project_root / "libs"
290 if libs_dir.file_exists then
291 toolcontext.exec_and_check(["cp", "-r", libs_dir, android_project_root], "Android project error")
292 end
293 end
294
295 redef fun write_makefile(compiler, compile_dir, cfiles)
296 do
297 # Do nothing, already done in `write_files`
298 end
299
300 redef fun compile_c_code(compiler, compile_dir)
301 do
302 var android_project_root = android_project_root.as(not null)
303 var release = toolcontext.opt_release.value
304
305 # Compile C code (and thus Nit)
306 toolcontext.exec_and_check(["ndk-build", "-s", "-j", "-C", android_project_root], "Android project error")
307
308 # Generate the apk
309 var args = ["ant", "-q", "-f", android_project_root+"/build.xml"]
310 if release then
311 args.add "release"
312 else args.add "debug"
313 toolcontext.exec_and_check(args, "Android project error")
314
315 # Move the apk to the target
316 var outname = outfile(compiler.mainmodule)
317
318 if release then
319 var apk_path = "{android_project_root}/bin/{compiler.mainmodule.name}-release-unsigned.apk"
320
321 # Sign APK
322 var keystore_path= "KEYSTORE".environ
323 var key_alias= "KEY_ALIAS".environ
324 var tsa_server= "TSA_SERVER".environ
325
326 if key_alias.is_empty then
327 toolcontext.fatal_error(null,
328 "Fatal Error: the environment variable `KEY_ALIAS` must be set to use the `--release` option on Android projects.")
329 end
330
331 args = ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path, key_alias]
332
333 ## Use a custom keystore
334 if not keystore_path.is_empty then args.add_all(["-keystore", keystore_path])
335
336 ## Use a TSA server
337 if not tsa_server.is_empty then args.add_all(["-tsa", tsa_server])
338
339 toolcontext.exec_and_check(args, "Android project error")
340
341 # Clean output file
342 if outname.to_path.exists then outname.to_path.delete
343
344 # Align APK
345 args = ["zipalign", "4", apk_path, outname]
346 toolcontext.exec_and_check(args, "Android project error")
347 else
348 # Move to the expected output path
349 args = ["mv", "{android_project_root}/bin/{compiler.mainmodule.name}-debug.apk", outname]
350 toolcontext.exec_and_check(args, "Android project error")
351 end
352 end
353 end
354
355 redef class JavaClassTemplate
356 redef fun write_to_files(compdir)
357 do
358 var jni_path = "jni/nit_compile/"
359 if compdir.has_suffix(jni_path) then
360 var path = "{compdir.substring(0, compdir.length-jni_path.length)}/src/"
361 return super(path)
362 else return super
363 end
364 end