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