Files
RubyMotion/lib/motion/project/template/android.rb
2014-06-11 00:28:59 +02:00

454 lines
18 KiB
Ruby

# encoding: utf-8
# Copyright (c) 2012, HipByte SPRL and contributors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
require 'motion/project/app'
App = Motion::Project::App
App.template = :android
require 'motion/project'
require 'motion/project/template/android/config'
desc "Create an application package file (.apk)"
task :build do
# Prepare build dir.
app_build_dir = App.config.versionized_build_dir
mkdir_p app_build_dir
# Compile Ruby files.
ruby = App.config.bin_exec('ruby')
init_func_n = 0
ruby_objs = []
bs_files = Dir.glob(File.join(App.config.versioned_datadir, 'BridgeSupport/*.bridgesupport'))
bs_files += App.config.vendored_bs_files
ruby_bs_flags = bs_files.map { |x| "--uses-bs \"#{x}\"" }.join(' ')
objs_build_dir = File.join(app_build_dir, 'obj', 'local', App.config.armeabi_directory_name)
kernel_bc = File.join(App.config.versioned_arch_datadir, "kernel-#{App.config.arch}.bc")
ruby_objs_changed = false
App.config.files.each do |ruby_path|
bc_path = File.join(objs_build_dir, ruby_path + '.bc')
init_func = "InitRubyFile#{init_func_n += 1}"
if !File.exist?(bc_path) \
or File.mtime(ruby_path) > File.mtime(bc_path) \
or File.mtime(ruby) > File.mtime(bc_path) \
or File.mtime(kernel_bc) > File.mtime(bc_path)
App.info 'Compile', ruby_path
FileUtils.mkdir_p(File.dirname(bc_path))
sh "VM_PLATFORM=android VM_KERNEL_PATH=\"#{kernel_bc}\" arch -i386 \"#{ruby}\" #{ruby_bs_flags} --emit-llvm \"#{bc_path}\" #{init_func} \"#{ruby_path}\""
ruby_objs_changed = true
end
ruby_objs << [bc_path, init_func]
end
# Generate payload main file.
payload_c_txt = <<EOS
// This file has been generated. Do not modify by hands.
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <jni.h>
#include <assert.h>
#include <android/log.h>
EOS
ruby_objs.each do |_, init_func|
payload_c_txt << "void #{init_func}(void *rcv, void *sel);\n"
end
payload_c_txt << <<EOS
void rb_vm_register_native_methods(void);
bool rb_vm_init(const char *app_package, JNIEnv *env);
jint
JNI_OnLoad(JavaVM *vm, void *reserved)
{
__android_log_write(ANDROID_LOG_DEBUG, "#{App.config.package_path}", "Loading payload");
JNIEnv *env = NULL;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
assert(env != NULL);
rb_vm_init("#{App.config.package_path}", env);
EOS
ruby_objs.each do |_, init_func|
payload_c_txt << " (*env)->PushLocalFrame(env, 32);\n"
payload_c_txt << " #{init_func}(NULL, NULL);\n"
payload_c_txt << " (*env)->PopLocalFrame(env, NULL);\n"
end
payload_c_txt << <<EOS
rb_vm_register_native_methods();
__android_log_write(ANDROID_LOG_DEBUG, "#{App.config.package_path}", "Loaded payload");
return JNI_VERSION_1_6;
}
EOS
payload_c = File.join(app_build_dir, 'jni/payload.c')
mkdir_p File.dirname(payload_c)
if !File.exist?(payload_c) or File.read(payload_c) != payload_c_txt
File.open(payload_c, 'w') { |io| io.write(payload_c_txt) }
end
# Compile and link payload library.
libs_abi_subpath = "lib/#{App.config.armeabi_directory_name}"
libpayload_subpath = "#{libs_abi_subpath}/#{App.config.payload_library_name}"
libpayload_path = "#{app_build_dir}/#{libpayload_subpath}"
if !File.exist?(libpayload_path) \
or ruby_objs_changed \
or File.mtime(File.join(App.config.versioned_arch_datadir, "librubymotion-static.a")) > File.mtime(libpayload_path)
payload_o = File.join(File.dirname(payload_c), 'payload.o')
if !File.exist?(payload_o) or File.mtime(payload_c) > File.mtime(payload_o)
sh "#{App.config.cc} #{App.config.cflags} -c \"#{payload_c}\" -o \"#{payload_o}\""
end
App.info 'Create', libpayload_path
FileUtils.mkdir_p(File.dirname(libpayload_path))
sh "#{App.config.cxx} #{App.config.ldflags} \"#{payload_o}\" #{ruby_objs.map { |o, _| "\"" + o + "\"" }.join(' ')} -o \"#{libpayload_path}\" #{App.config.ldlibs}"
end
# Create a build/libs -> build/lib symlink (important for ndk-gdb).
Dir.chdir(app_build_dir) { ln_s 'lib', 'libs' unless File.exist?('libs') }
# Create a build/jni/Android.mk file (important for ndk-gdb).
File.open("#{app_build_dir}/jni/Android.mk", 'w') { |io| }
# Copy the gdb server.
gdbserver_subpath = "#{libs_abi_subpath}/gdbserver"
gdbserver_path = "#{app_build_dir}/#{gdbserver_subpath}"
if !File.exist?(gdbserver_path)
App.info 'Create', gdbserver_path
sh "/usr/bin/install -p #{App.config.ndk_path}/prebuilt/android-arm/gdbserver/gdbserver #{File.dirname(gdbserver_path)}"
end
# Create the gdb config file.
gdbconfig_path = "#{app_build_dir}/#{libs_abi_subpath}/gdb.setup"
if !File.exist?(gdbconfig_path)
App.info 'Create', gdbconfig_path
File.open(gdbconfig_path, 'w') do |io|
io.puts <<EOS
set solib-search-path #{libs_abi_subpath}
EOS
end
end
# Generate the Android manifest file.
android_manifest_txt = ''
android_manifest_txt << <<EOS
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="#{App.config.package}" android:versionCode="#{App.config.version_code}" android:versionName="#{App.config.version_name}">
<uses-sdk android:minSdkVersion="#{App.config.api_version}"/>
EOS
App.config.manifest_xml_lines(nil).each { |line| android_manifest_txt << "\t" + line + "\n" }
android_manifest_txt << <<EOS
<application android:label="#{App.config.name}" android:debuggable="#{App.config.development? ? 'true' : 'false'}" #{App.config.icon ? ('android:icon="@drawable/' + App.config.icon + '"') : ''}>
EOS
App.config.manifest_xml_lines('application').each { |line| android_manifest_txt << "\t\t" + line + "\n" }
android_manifest_txt << <<EOS
<activity android:name="#{App.config.main_activity}" android:label="#{App.config.name}" android:configChanges="keyboardHidden|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
EOS
(App.config.sub_activities.uniq - [App.config.main_activity]).each do |activity|
android_manifest_txt << <<EOS
<activity android:name="#{activity}" android:label="#{activity}" android:parentActivityName="#{App.config.main_activity}">
<meta-data android:name="android.support.PARENT_ACTIVITY" android:value="#{App.config.main_activity}"/>
</activity>
EOS
end
android_manifest_txt << <<EOS
</application>
</manifest>
EOS
android_manifest = File.join(app_build_dir, 'AndroidManifest.xml')
if !File.exist?(android_manifest) or File.read(android_manifest) != android_manifest_txt
App.info 'Create', android_manifest
File.open(android_manifest, 'w') { |io| io.write(android_manifest_txt) }
end
# Create java files based on the classes map files.
java_classes = {}
Dir.glob(objs_build_dir + '/**/*.map') do |map|
txt = File.read(map)
current_class = nil
txt.each_line do |line|
if md = line.match(/^([^\s]+)\s*:\s*([^\s]+)\s*<([^>]*)>$/)
current_class = java_classes[md[1]]
if current_class
if current_class[:super] != md[2]
$stderr.puts "Class `#{md[1]}' already defined with a different super class (`#{current_class[:super]}')"
exit 1
end
else
infs = md[3].split(',').map { |x| x.strip }
current_class = {:super => md[2], :methods => [], :interfaces => infs}
java_classes[md[1]] = current_class
end
elsif md = line.match(/^\t(.+)$/)
if current_class == nil
$stderr.puts "Method definition outside class definition"
exit 1
end
current_class[:methods] << md[1]
else
$stderr.puts "Ignoring line: #{line}"
end
end
end
java_dir = File.join(app_build_dir, 'java')
java_app_package_dir = File.join(java_dir, *App.config.package.split(/\./))
mkdir_p java_app_package_dir
java_classes.each do |name, klass|
java_file_txt = ''
java_file_txt << <<EOS
// This file has been generated automatically. Do not edit.
package #{App.config.package};
EOS
java_file_txt << "public class #{name} extends #{klass[:super]}"
if klass[:interfaces].size > 0
java_file_txt << " implements #{klass[:interfaces].join(', ')}"
end
java_file_txt << " {\n"
klass[:methods].each do |method|
java_file_txt << "\t#{method}\n"
end
if name == App.config.main_activity
java_file_txt << "\tstatic {\n\t\tSystem.load(\"#{App.config.payload_library_name}\");\n\t}\n"
end
java_file_txt << "}\n"
java_file = File.join(java_app_package_dir, name + '.java')
if !File.exist?(java_file) or File.read(java_file) != java_file_txt
File.open(java_file, 'w') { |io| io.write(java_file_txt) }
end
end
# Create R.java files.
android_jar = "#{App.config.sdk_path}/platforms/android-#{App.config.api_version}/android.jar"
resources_dirs = []
App.config.resources_dirs.flatten.each do |dir|
next unless File.exist?(dir)
next unless File.directory?(dir)
resources_dirs << dir
end
all_resources = (resources_dirs + App.config.vendored_projects.map { |x| x[:resources] }.compact)
aapt_resources_flags = all_resources.map { |x| '-S "' + x + '"' }.join(' ')
r_java_mtime = Dir.glob(java_dir + '/**/R.java').map { |x| File.mtime(x) }.max
if !r_java_mtime or all_resources.any? { |x| Dir.glob(x + '/**/*').any? { |y| File.mtime(y) > r_java_mtime } }
extra_packages = App.config.vendored_projects.map { |x| x[:package] }.compact.map { |x| "--extra-packages #{x}" }.join(' ')
sh "\"#{App.config.build_tools_dir}/aapt\" package -f -M \"#{android_manifest}\" #{aapt_resources_flags} -I \"#{android_jar}\" -m -J \"#{java_dir}\" #{extra_packages} --auto-add-overlay"
end
# Compile java files.
vendored_jars = App.config.vendored_projects.map { |x| x[:jar] }
vendored_jars += [File.join(App.config.versioned_datadir, 'rubymotion.jar')]
classes_dir = File.join(app_build_dir, 'classes')
mkdir_p classes_dir
class_path = [classes_dir, "#{App.config.sdk_path}/tools/support/annotations.jar", *vendored_jars].map { |x| "\"#{x}\"" }.join(':')
classes_changed = false
Dir.glob(File.join(app_build_dir, 'java', '**', '*.java')).each do |java_path|
paths = java_path.split('/')
paths[paths.index('java')] = 'classes'
paths[-1].sub!(/\.java$/, '.class')
java_class_path = paths.join('/')
class_name = File.basename(java_path, '.java')
if !java_classes.has_key?(class_name) and class_name != 'R'
# This .java file is not referred in the classes map, so it must have been created in the past. We remove it as well as its associated .class file (if any).
rm_rf java_path
rm_rf java_class_path
classes_changed = true
next
end
if !File.exist?(java_class_path) or File.mtime(java_path) > File.mtime(java_class_path)
App.info 'Create', java_class_path
sh "/usr/bin/javac -d \"#{classes_dir}\" -classpath #{class_path} -sourcepath \"#{java_dir}\" -target 1.5 -bootclasspath \"#{android_jar}\" -encoding UTF-8 -g -source 1.5 \"#{java_path}\""
classes_changed = true
end
end
# Generate the dex file.
dex_classes = File.join(app_build_dir, 'classes.dex')
if !File.exist?(dex_classes) \
or File.mtime(App.config.project_file) > File.mtime(dex_classes) \
or classes_changed \
or vendored_jars.any? { |x| File.mtime(x) > File.mtime(dex_classes) }
App.info 'Create', dex_classes
sh "\"#{App.config.build_tools_dir}/dx\" --dex --output \"#{dex_classes}\" \"#{classes_dir}\" \"#{App.config.sdk_path}/tools/support/annotations.jar\" #{vendored_jars.join(' ')}"
end
keystore = nil
if App.config.development?
# Create the debug keystore if needed.
keystore = File.expand_path('~/.android/debug.keystore')
unless File.exist?(keystore)
App.info 'Create', keystore
sh "/usr/bin/keytool -genkeypair -alias androiddebugkey -keypass android -keystore \"#{keystore}\" -storepass android -dname \"CN=Android Debug,O=Android,C=US\" -validity 9999"
end
else
keystore = App.config.release_keystore_path
App.fail "app.release_keystore(path, alias_name) must be called when doing a release build" unless keystore
end
# Generate the APK file.
archive = App.config.apk_path
if !File.exist?(archive) \
or File.mtime(dex_classes) > File.mtime(archive) \
or File.mtime(libpayload_path) > File.mtime(archive) \
or File.mtime(android_manifest) > File.mtime(archive) \
or resources_dirs.any? { |x| File.mtime(x) > File.mtime(archive) }
App.info 'Create', archive
assets_dirs = []
App.config.assets_dirs.flatten.each do |dir|
next unless File.exist?(dir)
next unless File.directory?(dir)
assets_dirs << dir
end
assets_flags = assets_dirs.map { |x| '-A "' + x + '"' }.join(' ')
sh "\"#{App.config.build_tools_dir}/aapt\" package -f -M \"#{android_manifest}\" #{assets_flags} #{aapt_resources_flags} -I \"#{android_jar}\" -F \"#{archive}\" --auto-add-overlay"
Dir.chdir(app_build_dir) do
[File.basename(dex_classes), libpayload_subpath, gdbserver_subpath].each do |file|
line = "\"#{App.config.build_tools_dir}/aapt\" add -f \"#{File.basename(archive)}\" \"#{file}\""
line << " > /dev/null" unless Rake.application.options.trace
sh line
end
end
App.info 'Sign', archive
if App.config.development?
sh "/usr/bin/jarsigner -storepass android -keystore \"#{keystore}\" \"#{archive}\" androiddebugkey"
else
sh "/usr/bin/jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore \"#{keystore}\" \"#{archive}\" \"#{App.config.release_keystore_alias}\""
end
App.info 'Align', archive
sh "\"#{App.config.sdk_path}/tools/zipalign\" -f 4 \"#{archive}\" \"#{archive}-aligned\""
sh "/bin/mv \"#{archive}-aligned\" \"#{archive}\""
end
end
desc "Create an application package file (.apk) for release (Google Play)"
task :release do
App.config_without_setup.build_mode = :release
App.config_without_setup.distribution_mode = true
Rake::Task["build"].invoke
end
def adb_mode_flag(mode)
case mode
when :emulator
'-e'
when :device
'-d'
else
raise
end
end
def adb_path
"#{App.config.sdk_path}/platform-tools/adb"
end
def install_apk(mode)
App.info 'Install', App.config.apk_path
line = "\"#{adb_path}\" #{adb_mode_flag(mode)} install -r \"#{App.config.apk_path}\""
line << " > /dev/null" unless Rake.application.options.trace
sh line
end
def run_apk(mode)
if ENV['debug']
App.fail "debug mode not implemented yet"
=begin
Dir.chdir(App.config.build_dir) do
App.info 'Debug', App.config.apk_path
sh "\"#{App.config.ndk_path}/ndk-gdb\" #{adb_mode_flag(mode)} --adb=\"#{adb_path}\" --start"
end
=end
else
# Clear log.
sh "\"#{adb_path}\" #{adb_mode_flag(mode)} logcat -c"
# Start main activity.
activity_path = "#{App.config.package}/.#{App.config.main_activity}"
App.info 'Start', activity_path
line = "\"#{adb_path}\" #{adb_mode_flag(mode)} shell am start -a android.intent.action.MAIN -n #{activity_path}"
line << " > /dev/null" unless Rake.application.options.trace
sh line
Signal.trap('INT') do
# Kill the app on ^C.
if `\"#{adb_path}\" -d shell ps`.include?(App.config.package)
sh "\"#{adb_path}\" #{adb_mode_flag(mode)} shell am force-stop #{App.config.package}"
end
exit 0
end
# Show logs.
sh "\"#{adb_path}\" #{adb_mode_flag(mode)} logcat -s #{App.config.logs_components.join(' ')}"
end
end
namespace 'emulator' do
desc "Create the Android Virtual Device for the emulator"
task :create_avd do
all_targets = `\"#{App.config.sdk_path}/tools/android\" list avd --compact`.split(/\n/)
if !all_targets.include?(App.config.avd_config[:name])
sh "/bin/echo -n \"no\" | \"#{App.config.sdk_path}/tools/android\" create avd --name \"#{App.config.avd_config[:name]}\" --target \"#{App.config.avd_config[:target]}\" --abi \"#{App.config.avd_config[:abi]}\" --snapshot"
end
end
desc "Start the emulator in the background"
task :start_avd do
avd = (ENV['avd'] || App.config.avd_config[:name])
unless `/bin/ps -a`.split(/\n/).any? { |x| x.include?('emulator64-arm') and x.include?(avd) }
Rake::Task["emulator:create_avd"].invoke
sh "\"#{App.config.sdk_path}/tools/emulator\" -avd \"#{avd}\" &"
sh "\"#{App.config.sdk_path}/platform-tools/adb\" -e wait-for-device"
end
end
desc "Install the app in the emulator"
task :install do
install_apk(:emulator)
end
desc "Start the app's main intent in the emulator"
task :start => ['build', 'emulator:start_avd', 'emulator:install'] do
run_apk(:emulator)
end
end
namespace 'device' do
desc "Install the app in the device"
task :install do
install_apk(:device)
end
desc "Start the app's main intent in the device"
task :start do
run_apk(:device)
end
end
desc "Build the app then run it in the device"
task :device => ['build', 'device:install', 'device:start']
desc "Build the app then run it in the emulator"
task :default => 'emulator:start'