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