Files
RubyMotion/lib/motion/project/template/android.rb
2014-03-26 17:44:11 +01:00

355 lines
14 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
# Compile Ruby files.
ruby = File.join(App.config.motiondir, 'bin/ruby')
init_func_n = 0
ruby_objs = []
bs_files = Dir.glob(File.join(App.config.versioned_datadir, 'BridgeSupport/*.bridgesupport'))
ruby_bs_flags = bs_files.map { |x| "--uses-bs \"#{x}\"" }.join(' ')
objs_build_dir = File.join(App.config.build_dir, 'obj', 'local', App.config.armeabi_directory_name)
Dir.glob("./app/**/*.rb").each do |ruby_path|
App.info 'Compile', ruby_path
init_func = "InitRubyFile#{init_func_n += 1}"
bc_path = File.join(objs_build_dir, ruby_path + '.bc')
FileUtils.mkdir_p(File.dirname(bc_path))
sh "VM_PLATFORM=android VM_KERNEL_PATH=\"#{App.config.versioned_arch_datadir}/kernel-#{App.config.arch}.bc\" arch -i386 \"#{ruby}\" #{ruby_bs_flags} --emit-llvm \"#{bc_path}\" #{init_func} \"#{ruby_path}\""
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 << " #{init_func}(NULL, 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.config.build_dir, 'jni/payload.c')
mkdir_p File.dirname(payload_c)
File.open(payload_c, 'w') { |io| io.write(payload_c_txt) }
# Compile and link payload library.
rm_rf "#{App.config.build_dir}/lib"
libs_abi_subpath = "lib/#{App.config.armeabi_directory_name}"
libpayload_subpath = "#{libs_abi_subpath}/#{App.config.payload_library_name}"
libpayload_path = "#{App.config.build_dir}/#{libpayload_subpath}"
payload_o = File.join(File.dirname(payload_c), 'payload.o')
App.info 'Create', libpayload_path
sh "#{App.config.cc} #{App.config.cflags} -c \"#{payload_c}\" -o \"#{payload_o}\""
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}"
# Create a build/libs -> build/lib symlink (important for ndk-gdb).
Dir.chdir(App.config.build_dir) { ln_s 'lib', 'libs' unless File.exist?('libs') }
# Create a build/jni/Android.mk file (important for ndk-gdb).
File.open("#{App.config.build_dir}/jni/Android.mk", 'w') { |io| }
# Copy the gdb server.
gdbserver_subpath = "#{libs_abi_subpath}/gdbserver"
gdbserver_path = "#{App.config.build_dir}/#{gdbserver_subpath}"
App.info 'Create', gdbserver_path
sh "/usr/bin/install -p #{App.config.ndk_path}/prebuilt/android-arm/gdbserver/gdbserver #{File.dirname(gdbserver_path)}"
# Create the gdb config file.
gdbconfig_path = "#{App.config.build_dir}/#{libs_abi_subpath}/gdb.setup"
App.info 'Create', gdbconfig_path
File.open(gdbconfig_path, 'w') do |io|
io.puts <<EOS
set solib-search-path #{libs_abi_subpath}
EOS
end
# Create java 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]+)$/)
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
current_class = {:super => md[2], :methods => []}
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.config.build_dir, 'java')
rm_rf java_dir
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 = File.join(java_app_package_dir, name + '.java')
App.info 'Create', java_file
File.open(java_file, 'w') do |io|
io.puts "// This file has been generated. Do not edit by hands."
io.puts "package #{App.config.package};"
io.puts "public class #{name} extends #{klass[:super]} {"
klass[:methods].each do |method|
io.puts "\t#{method};"
end
if name == App.config.main_activity
io.puts "\tstatic {\n\t\tSystem.load(\"#{App.config.payload_library_name}\");\n\t}"
end
io.puts "}"
end
end
# Compile java files.
android_jar = "#{App.config.sdk_path}/platforms/android-#{App.config.api_version}/android.jar"
vendored_jars = App.config.vendored_jars
classes_dir = File.join(App.config.build_dir, 'classes')
FileUtils.mkdir_p(classes_dir)
class_path = [classes_dir, "#{App.config.sdk_path}/tools/support/annotations.jar", *vendored_jars].map { |x| "\"#{x}\"" }.join(':')
rebuild_dex_classes = false
Dir.glob(File.join(App.config.build_dir, 'java', '**', '*.java')).each do |java_path|
paths = java_path.split('/')
paths[paths.index('java')] = 'classes'
paths[-1].sub!(/\.java$/, '.class')
class_path = paths.join('/')
if !File.exist?(class_path) or File.mtime(java_path) > File.mtime(class_path)
App.info 'Compile', java_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}\""
rebuild_dex_classes = true
end
end
# Generate the dex file.
dex_classes = File.join(App.config.build_dir, 'classes.dex')
if !File.exist?(dex_classes) or rebuild_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
# Generate the Android manifest file.
android_manifest = File.join(App.config.build_dir, 'AndroidManifest.xml')
App.info 'Create', android_manifest
File.open(android_manifest, 'w') do |io|
io.print <<EOS
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="#{App.config.package}" android:versionCode="1" android:versionName="1.0">
<uses-sdk android:minSdkVersion="#{App.config.api_version}"/>
<application android:label="#{App.config.name}" android:debuggable="true">
<activity android:name="#{App.config.main_activity}" android:label="#{App.config.name}">
<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|
io.print <<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
io.print <<EOS
</application>
</manifest>
EOS
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)
App.info 'Create', archive
resource_flags = App.config.resources_dirs.map { |x| '-S "' + x + '"' }.join(' ')
sh "\"#{App.config.build_tools_dir}/aapt\" package -f -M \"#{android_manifest}\" #{resource_flags} -I \"#{android_jar}\" -F \"#{archive}\""
Dir.chdir(App.config.build_dir) do
[File.basename(dex_classes), libpayload_subpath, gdbserver_subpath].each do |file|
line = "\"#{App.config.build_tools_dir}/aapt\" add -f \"../#{archive}\" \"#{file}\""
line << " > /dev/null" unless Rake.application.options.trace
sh line
end
end
# Create the debug keystore if needed.
debug_keystore = File.expand_path('~/.android/debug.keystore')
unless File.exist?(debug_keystore)
App.info 'Create', debug_keystore
sh "/usr/bin/keytool -genkeypair -alias androiddebugkey -keypass android -keystore \"#{debug_keystore}\" -storepass android -dname \"CN=Android Debug,O=Android,C=US\" -validity 9999"
end
App.info 'Sign', archive
sh "/usr/bin/jarsigner -storepass android -keystore \"#{debug_keystore}\" \"#{archive}\" androiddebugkey"
App.info 'Align', archive
sh "\"#{App.config.sdk_path}/tools/zipalign\" -f 4 \"#{archive}\" \"#{archive}-aligned\""
sh "/bin/mv \"#{archive}-aligned\" \"#{archive}\""
end
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']
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
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.package_path}:I"
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
unless `/bin/ps -a`.split(/\n/).any? { |x| x.include?('emulator64-arm') and x.include?('RubyMotion') }
Rake::Task["emulator:create_avd"].invoke
sh "\"#{App.config.sdk_path}/tools/emulator\" -avd \"#{App.config.avd_config[:name]}\" &"
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'