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