f4b04a6b12bb09dd9848f64b8cd9f0d69fb923a1
[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 delegating makefile
144 dir = "{android_project_root}/jni/"
145 """
146 include $(call all-subdir-makefiles)
147 """.write_to_file("{dir}/Android.mk")
148
149 # Gather ldflags for Android
150 var ldflags = new Array[String]
151 var platform_name = "android"
152 for mmodule in compiler.mainmodule.in_importation.greaters do
153 if mmodule.ldflags.keys.has(platform_name) then
154 ldflags.add_all mmodule.ldflags[platform_name]
155 end
156 end
157
158 ### generate makefile into "{compile_dir}/Android.mk"
159 dir = compile_dir
160 """
161 LOCAL_PATH := $(call my-dir)
162 include $(CLEAR_VARS)
163
164 LOCAL_CFLAGS := -D ANDROID -D WITH_LIBGC
165 LOCAL_MODULE := main
166 LOCAL_SRC_FILES := \\
167 {{{cfiles.join(" \\\n")}}}
168 LOCAL_LDLIBS := {{{ldflags.join(" ")}}} libgc.a
169 LOCAL_STATIC_LIBRARIES := android_native_app_glue png
170
171 include $(BUILD_SHARED_LIBRARY)
172
173 $(call import-module,android/native_app_glue)
174 """.write_to_file("{dir}/Android.mk")
175
176 ### generate AndroidManifest.xml
177 dir = android_project_root
178 """<?xml version="1.0" encoding="utf-8"?>
179 <!-- BEGIN_INCLUDE(manifest) -->
180 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
181 package="{{{app_package}}}"
182 android:versionCode="{{{project.version_code}}}"
183 android:versionName="{{{app_version}}}">
184
185 <!-- This is the platform API where NativeActivity was introduced. -->
186 <uses-sdk
187 android:minSdkVersion="{{{app_min_api}}}"
188 android:targetSdkVersion="{{{app_target_api}}}"
189 {{{app_max_api}}} />
190
191 <application
192 android:label="@string/app_name"
193 android:hasCode="true"
194 android:debuggable="{{{not release}}}"
195 {{{icon_declaration}}}
196 android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation">
197
198 <!-- Our activity is the built-in NativeActivity framework class.
199 This will take care of integrating with our NDK code. -->
200 <activity android:name="android.app.NativeActivity"
201 android:label="@string/app_name"
202 {{{project.manifest_activity_attributes.join("\n")}}}
203 {{{icon_declaration}}}>
204 <!-- Tell NativeActivity the name of our .so -->
205 <meta-data android:name=\"android.app.lib_name\"
206 android:value=\"main\" />
207 <intent-filter>
208 <action android:name="android.intent.action.MAIN" />
209 <category android:name="android.intent.category.LAUNCHER" />
210 </intent-filter>
211 </activity>
212
213 {{{project.manifest_application_lines.join("\n")}}}
214
215 </application>
216
217 {{{project.manifest_lines.join("\n")}}}
218
219 </manifest>
220 <!-- END_INCLUDE(manifest) -->
221 """.write_to_file("{dir}/AndroidManifest.xml")
222
223 ### Link to png sources
224 # libpng is not available on Android NDK
225 # FIXME make obtionnal when we have alternatives to mnit
226 var nit_dir = toolcontext.nit_dir
227 var share_dir = nit_dir/"share/"
228 if not share_dir.file_exists then
229 print "Android project error: Nit share directory not found, please use the environment variable NIT_DIR"
230 exit 1
231 end
232 share_dir = share_dir.realpath
233 var target_png_dir = "{android_project_root}/jni/png"
234 if not target_png_dir.file_exists then
235 toolcontext.exec_and_check(["ln", "-s", "{share_dir}/png/", target_png_dir], "Android project error")
236 end
237
238 # Ensure that android-setup-libgc.sh has been executed
239 if not "{share_dir}/libgc/lib".file_exists then
240 toolcontext.exec_and_check(["{share_dir}/libgc/android-setup-libgc.sh"], "Android project error")
241 end
242
243 # Copy GC files
244 toolcontext.exec_and_check(["cp", "{share_dir}/libgc/lib/libgc.a", "{android_project_root}/libgc.a"], "Android project error")
245 toolcontext.exec_and_check(["ln", "-s", "{share_dir}/libgc/include/gc/", "{android_project_root}/jni/nit_compile/gc"], "Android project error")
246
247 ### Link to assets (for mnit and others)
248 # This will be accessed from `android_project_root`
249 var assets_dir
250 if compiler.mainmodule.location.file != null then
251 # it is a real file, use "{file}/../assets"
252 assets_dir = "{compiler.mainmodule.location.file.filename.dirname}/../assets"
253 else
254 # probably used -m, use "."
255 assets_dir = "assets"
256 end
257 if assets_dir.file_exists then
258 assets_dir = assets_dir.realpath
259 var target_assets_dir = "{android_project_root}/assets"
260 if not target_assets_dir.file_exists then
261 toolcontext.exec_and_check(["ln", "-s", assets_dir, target_assets_dir], "Android project error")
262 end
263 end
264
265 ### Copy resources and libs where expected by the SDK
266 var project_root
267 if compiler.mainmodule.location.file != null then
268 # it is a real file, use "{file}/../res"
269 project_root = "{compiler.mainmodule.location.file.filename.dirname}/.."
270 else
271 # probably used -m, use "."
272 project_root = "."
273 end
274
275 # Android resources folder
276 var res_dir = project_root / "res"
277 if res_dir.file_exists then
278 # copy the res folder to .nit_compile
279 res_dir = res_dir.realpath
280 toolcontext.exec_and_check(["cp", "-R", res_dir, android_project_root], "Android project error")
281 end
282
283 if not res_dir.file_exists or not "{res_dir}/values/strings.xml".file_exists then
284 # Create our own custom `res/values/string.xml` with the App name
285 """<?xml version="1.0" encoding="utf-8"?>
286 <resources>
287 <string name="app_name">{{{app_name}}}</string>
288 </resources>""".write_to_file "{dir}/res/values/strings.xml"
289 end
290
291 # Android libs folder
292 var libs_dir = project_root / "libs"
293 if libs_dir.file_exists then
294 toolcontext.exec_and_check(["cp", "-r", libs_dir, android_project_root], "Android project error")
295 end
296 end
297
298 redef fun write_makefile(compiler, compile_dir, cfiles)
299 do
300 # Do nothing, already done in `write_files`
301 end
302
303 redef fun compile_c_code(compiler, compile_dir)
304 do
305 var android_project_root = android_project_root.as(not null)
306 var short_project_name = compiler.mainmodule.name.replace("-", "_")
307 var release = toolcontext.opt_release.value
308
309 # Compile C code (and thus Nit)
310 toolcontext.exec_and_check(["ndk-build", "-s", "-j", "-C", android_project_root], "Android project error")
311
312 # Generate the apk
313 var args = ["ant", "-q", "-f", android_project_root+"/build.xml"]
314 if release then
315 args.add "release"
316 else args.add "debug"
317 toolcontext.exec_and_check(args, "Android project error")
318
319 # Move the apk to the target
320 var outname = outfile(compiler.mainmodule)
321
322 if release then
323 var apk_path = "{android_project_root}/bin/{short_project_name}-release-unsigned.apk"
324
325 # Sign APK
326 var keystore_path= "KEYSTORE".environ
327 var key_alias= "KEY_ALIAS".environ
328 var tsa_server= "TSA_SERVER".environ
329
330 if key_alias.is_empty then
331 toolcontext.fatal_error(null,
332 "Fatal Error: the environment variable `KEY_ALIAS` must be set to use the `--release` option on Android projects.")
333 end
334
335 args = ["jarsigner", "-sigalg", "MD5withRSA", "-digestalg", "SHA1", apk_path, key_alias]
336
337 ## Use a custom keystore
338 if not keystore_path.is_empty then args.add_all(["-keystore", keystore_path])
339
340 ## Use a TSA server
341 if not tsa_server.is_empty then args.add_all(["-tsa", tsa_server])
342
343 toolcontext.exec_and_check(args, "Android project error")
344
345 # Clean output file
346 if outname.to_path.exists then outname.to_path.delete
347
348 # Align APK
349 args = ["zipalign", "4", apk_path, outname]
350 toolcontext.exec_and_check(args, "Android project error")
351 else
352 # Move to the expected output path
353 args = ["mv", "{android_project_root}/bin/{short_project_name}-debug.apk", outname]
354 toolcontext.exec_and_check(args, "Android project error")
355 end
356 end
357 end
358
359 redef class JavaClassTemplate
360 redef fun write_to_files(compdir)
361 do
362 var jni_path = "jni/nit_compile/"
363 if compdir.has_suffix(jni_path) then
364 var path = "{compdir.substring(0, compdir.length-jni_path.length)}/src/"
365 return super(path)
366 else return super
367 end
368 end