# 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 = < #include #include #include #include #include EOS ruby_objs.each do |_, init_func| payload_c_txt << "void #{init_func}(void *rcv, void *sel);\n" end payload_c_txt << <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 << < 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 App.config.manifest_xml_lines(nil).each { |line| android_manifest_txt << "\t" + line + "\n" } android_manifest_txt << < EOS App.config.manifest_xml_lines('application').each { |line| android_manifest_txt << "\t\t" + line + "\n" } android_manifest_txt << < EOS (App.config.sub_activities.uniq - [App.config.main_activity]).each do |activity| android_manifest_txt << < EOS end android_manifest_txt << < 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 << < 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" all_resources = (App.config.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 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 App.config.resources_dirs.any? { |x| File.mtime(x) > File.mtime(archive) } App.info 'Create', archive assets_flags = App.config.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 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'