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