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