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