Created
December 25, 2014 04:46
-
-
Save bbtfr/204a1c61182027f17081 to your computer and use it in GitHub Desktop.
Revisions
-
bbtfr created this gist
Dec 25, 2014 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,614 @@ # 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 # 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="#{x=App.config.version_code}" android:versionName="#{App.config.version_name}"> <uses-sdk android:minSdkVersion="#{App.config.api_version}" android:targetSdkVersion="#{App.config.target_api_version}"/> EOS # Application permissions. permissions = Array(App.config.permissions) if App.config.development? # In development mode, we need the INTERNET permission in order to create # the REPL socket. permissions |= ['android.permission.INTERNET'] end permissions.each do |permission| permission = "android.permission.#{permission.to_s.upcase}" if permission.is_a?(Symbol) android_manifest_txt << <<EOS <uses-permission android:name="#{permission}"></uses-permission> EOS end # Application features. features = Array(App.config.features) features.each do |feature| android_manifest_txt << <<EOS <uses-feature android:name="#{feature}"></uses-feature> EOS end # Custom manifest entries. 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 + '"') : ''}#{App.config.theme ? (' android:theme="@style/' + App.config.theme + '"') : ''}#{App.config.application_class ? (' android:name="' + App.config.application_class + '"') : ''}> EOS App.config.manifest_xml_lines('application').each { |line| android_manifest_txt << "\t\t" + line + "\n" } # Main activity. android_manifest_txt << <<EOS <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 # Sub-activities. (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 # Services. services = Array(App.config.services) services.each do |service| android_manifest_txt << <<EOS <service android:name="#{service}" android:exported="false"></service> 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 R.java files. 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 r_bs = File.join(app_build_dir, 'R.bridgesupport') android_jar = "#{App.config.sdk_path}/platforms/android-#{App.config.target_api_version}/android.jar" grab_directories = lambda do |ary| ary.flatten.select do |dir| File.exist?(dir) and File.directory?(dir) end end assets_dirs = grab_directories.call(App.config.assets_dirs) aapt_assets_flags = assets_dirs.map { |x| '-A "' + x + '"' }.join(' ') resources_dirs = grab_directories.call(App.config.resources_dirs) 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 bs_files = [] classes_changed = false 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_assets_flags} #{aapt_resources_flags} -I \"#{android_jar}\" -m -J \"#{java_dir}\" #{extra_packages} --auto-add-overlay" r_java = Dir.glob(java_dir + '/**/R.java') classes_dir = File.join(app_build_dir, 'classes') mkdir_p classes_dir r_java.each do |java_path| sh "/usr/bin/javac -d \"#{classes_dir}\" -classpath #{classes_dir} -sourcepath \"#{java_dir}\" -target 1.6 -bootclasspath \"#{android_jar}\" -encoding UTF-8 -g -source 1.6 \"#{java_path}\"" end r_classes = Dir.glob(classes_dir + '/**/R\$*[a-z]*.class').map { |c| "'#{c}'" } sh "#{App.config.bin_exec('android/gen_bridge_metadata')} #{r_classes.join(' ')} -o \"#{r_bs}\" " classes_changed = true end bs_files << r_bs if File.exist?(r_bs) # Compile Ruby files. ruby = App.config.bin_exec('ruby') 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 = App.config.kernel_path ruby_objs_changed = false payload_rb_txt = '' App.config.files.each do |ruby_path| bc_path = File.join(objs_build_dir, ruby_path + '.bc') init_func = "MREP_" + `/bin/echo \"#{File.expand_path(bc_path)}\" | /usr/bin/openssl sha1`.strip 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 # Get java classes info based on the classes map files. java_classes = {} Dir.glob(objs_build_dir + '/app/**/*.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 # Class is already exported, make sure the super classes match. 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 # Export a new class. 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 declaration outside class definition" exit 1 end method_line = md[1] add_method = false if method_line.include?('{') # A method definition (ex. a constructor), always include it. add_method = true else # Strip 'public native X' (where X is the return type). ary = method_line.split(/\s+/) if ary[0] == 'public' and ary[1] == 'native' method_line2 = ary[3..-1].join(' ') # Make sure we are not trying to declare the same method twice. if current_class[:methods].all? { |x| x.index(method_line2) != x.size - method_line2.size } add_method = true end else # Probably something else (what could it be?). add_method = true end end current_class[:methods] << method_line if add_method else $stderr.puts "Ignoring line: #{line}" end end end # Load extension files (any .java file in app directory). Dir.glob(File.join(App.config.project_dir, "app/**/*.java")).each do |java_ext| java_file_txt = File.read(java_ext) if java_file_txt.start_with? "package #{App.config.package};" class_name = java_file_txt[/class \S+/].sub('class ', '') payload_rb_txt << <<EOS unless java_classes[class_name] class #{class_name} end EOS java_classes[class_name] = { raw: java_file_txt } else class_name = File.basename(java_ext).sub(/\.java$/, '') klass = java_classes[class_name] unless klass # Convert underscore-case to CamelCase (ex. my_service -> MyService). class_name = class_name.split('_').map { |e| e.capitalize }.join klass = java_classes[class_name] || java_classes[class_name.upcase] end App.fail "Java file `#{java_ext}' extends a class that was not discovered by the compiler" unless klass (klass[:extensions] ||= "").concat(java_file_txt) end end payload_rb = File.join(objs_build_dir, 'payload.rb') if !File.exist?(payload_rb) or File.read(payload_rb) != payload_rb_txt App.info 'Create', payload_rb FileUtils.mkdir_p(objs_build_dir) File.open(payload_rb, 'w') { |io| io.write(payload_rb_txt) } end bc_path = File.join(objs_build_dir, 'payload.rb.bc') init_func = "MREP_" + `/bin/echo \"#{File.expand_path(bc_path)}\" | /usr/bin/openssl sha1`.strip if !File.exist?(bc_path) \ or File.mtime(payload_rb) > File.mtime(bc_path) \ or File.mtime(ruby) > File.mtime(bc_path) \ or File.mtime(kernel_bc) > File.mtime(bc_path) App.info 'Compile', payload_rb 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} \"#{payload_rb}\"" ruby_objs_changed = true end ruby_objs << [bc_path, init_func] # 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> extern "C" { void rb_vm_register_native_methods(void); bool rb_vm_init(const char *app_package, const char *rm_env, const char *rm_version, JNIEnv *env); void *rb_vm_top_self(void); void rb_rb2oc_exc_handler(void); EOS ruby_objs.each do |_, init_func| payload_c_txt << <<EOS void *#{init_func}(void *rcv, void *sel); EOS end payload_c_txt << <<EOS } extern "C" 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((void **)&env, JNI_VERSION_1_6) != JNI_OK) { return -1; } assert(env != NULL); rb_vm_init("#{App.config.package_path}", "#{App.config.rubymotion_env_value}", "#{Motion::Version}", env); void *top_self = rb_vm_top_self(); EOS ruby_objs.each do |ruby_obj, init_func| payload_c_txt << <<EOS try { env->PushLocalFrame(32); #{init_func}(top_self, NULL); env->PopLocalFrame(NULL); } catch (...) { __android_log_write(ANDROID_LOG_ERROR, "#{App.config.package_path}", "Uncaught exception when initializing `#{File.basename(ruby_obj).sub(/\.bc$/, '')}' scope -- aborting"); return -1; } EOS 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 # Append cpp files based on java classes info (any .cpp file in app directory). Dir.glob(File.join(App.config.project_dir, "app/**/*.cpp")).each do |cpp_ext| class_name = File.basename(cpp_ext).sub(/\.cpp$/, '') klass = java_classes[class_name] unless klass # Convert underscore-case to CamelCase (ex. my_service -> MyService). class_name = class_name.split('_').map { |e| e.capitalize }.join klass = java_classes[class_name] || java_classes[class_name.upcase] end App.fail "Cpp file `#{cpp_ext}' extends a class that was not discovered by the compiler" unless klass payload_c_txt << File.read(cpp_ext) end payload_c = File.join(app_build_dir, 'jni/payload.cpp') 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_filename}" libpayload_path = "#{app_build_dir}/#{libpayload_subpath}" payload_o = File.join(File.dirname(payload_c), 'payload.o') 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) \ or File.mtime(payload_c) > File.mtime(payload_o) App.info 'Create', libpayload_path FileUtils.mkdir_p(File.dirname(libpayload_path)) sh "#{App.config.cc} #{App.config.cflags} -c \"#{payload_c}\" -o \"#{payload_o}\" #{App.config.extra_cflags}" sh "#{App.config.cxx} #{App.config.ldflags} \"#{payload_o}\" #{ruby_objs.map { |o, _| "\"" + o + "\"" }.join(' ')} -o \"#{libpayload_path}\" #{App.config.ldlibs} #{App.config.extra_ldflags}" 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 # Create java files based on java classes info java_classes.each do |name, klass| java_file_txt = '' if klass[:raw] java_file_txt << klass[:raw] else klass_super = klass[:super] klass_super = 'java.lang.Object' if klass_super == '$blank$' 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" if ext = klass[:extensions] java_file_txt << ext.gsub(/^/m, "\t") end klass[:methods].each do |method| java_file_txt << "\t#{method}\n" end if name == App.config.application_class or (App.config.application_class == nil and name == App.config.main_activity) # We need to insert code to load the payload library. It has to be done either in the main activity class or in the custom application class (if provided), as the later will be loaded first. java_file_txt << "\tstatic {\n\t\tjava.lang.System.loadLibrary(\"#{App.config.payload_library_name}\");\n\t}\n" end java_file_txt << "}\n" end 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 # Compile java files. classes_changed = false 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(':') 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.6 -bootclasspath \"#{android_jar}\" -encoding UTF-8 -g -source 1.6 \"#{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 FileUtils.mkdir_p(File.expand_path('~/.android')) 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 assets_dirs.any? { |x| File.mtime(x) > File.mtime(archive) } \ or resources_dirs.any? { |x| File.mtime(x) > File.mtime(archive) } App.info 'Create', archive sh "\"#{App.config.build_tools_dir}/aapt\" package -f -M \"#{android_manifest}\" #{aapt_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 -digestalg SHA1 -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.zipalign_path}\" -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 if mode == :device App.fail "Could not find a USB-connected device" if device_id.empty? device_version = device_api_version(device_id) app_api_version = App.config.api_version app_api_version = app_api_version == 'L' ? 20 : app_api_version.to_i if device_version < app_api_version App.fail "Cannot install an app built for API version #{App.config.api_version} on a device running API version #{device_version}" end end 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 device_api_version(device_id) api_version = `"#{adb_path}" -d -s "#{device_id}\" shell getprop ro.build.version.sdk` if $?.exitstatus == 0 api_version.to_i else App.fail "Could not retrieve the API version for the USB-connected device. Make sure that the cable is properly connected and that the computer is authorized on the device to use USB debugging." end end def device_id @device_id ||= `\"#{adb_path}\" -d devices| awk 'NR==1{next} length($1)>0{printf $1; exit}'` 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 # Show logs in a child process. adb_logs_pid = spawn "\"#{adb_path}\" #{adb_mode_flag(mode)} logcat -s #{App.config.logs_components.join(' ')}" at_exit do # Kill the logcat process. Process.kill('KILL', adb_logs_pid) # Kill the app (if it's still active). 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 end # Enable port forwarding for the REPL socket. sh "\"#{adb_path}\" #{adb_mode_flag(mode)} forward tcp:33333 tcp:33333" # Launch the REPL. sh "\"#{App.config.bin_exec('android/repl')}\" \"#{App.config.kernel_path}\" 0.0.0.0 33333" end end namespace 'emulator' do 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: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' This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,343 @@ # 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. module Motion; module Project; class AndroidConfig < Config register :android variable :sdk_path, :ndk_path, :avd_config, :package, :main_activity, :sub_activities, :api_version, :target_api_version, :arch, :assets_dirs, :icon, :theme, :logs_components, :version_code, :version_name, :permissions, :features, :services, :application_class, :extra_ldflags, :extra_cflags def initialize(project_dir, build_mode) super @avd_config = { :name => 'RubyMotion', :target => '1', :abi => 'armeabi-v7a' } @main_activity = 'MainActivity' @sub_activities = [] @arch = 'armv5te' @assets_dirs = [File.join(project_dir, 'assets')] @vendored_projects = [] @permissions = [] @features = [] @services = [] @manifest_entries = {} @release_keystore_path = nil @release_keystore_alias = nil @version_code = '1' @version_name = '1.0' @application_class = nil if path = ENV['RUBYMOTION_ANDROID_SDK'] @sdk_path = File.expand_path(path) end if path = ENV['RUBYMOTION_ANDROID_NDK'] @ndk_path = File.expand_path(path) end end def validate if !sdk_path or !File.exist?(sdk_path) App.fail "app.sdk_path should point to a valid Android SDK directory." end if !ndk_path or !File.exist?(ndk_path) App.fail "app.ndk_path should point to a valid Android NDK directory." end if api_version == nil or !File.exist?("#{sdk_path}//platforms/android-#{api_version}") App.fail "The Android SDK installed on your system does not support " + (api_version == nil ? "any API level" : "API level #{api_version}") + ". Run the `#{sdk_path}/tools/android' program to install missing API levels." end if !File.exist?("#{ndk_path}/platforms/android-#{api_version_ndk}") App.fail "The Android NDK installed on your system does not support API level #{api_version}. Switch to a lower API level or install a more recent NDK." end super end def zipalign_path @zipalign ||= begin ary = Dir.glob(File.join(sdk_path, 'build-tools/*/zipalign')) if ary.empty? path = File.join(sdk_path, 'tools/zipalign') unless File.exist?(path) App.fail "Can't locate `zipalign' tool. Make sure you properly installed the Android Build Tools package and try again." end path else ary.last end end end def package @package ||= 'com.yourcompany' + '.' + name.downcase.gsub(/\s/, '') end def package_path package.gsub('.', '/') end def latest_api_version @latest_api_version ||= begin versions = Dir.glob(sdk_path + '/platforms/android-*').map do |path| md = File.basename(path).match(/\d+$/) md ? md[0] : nil end.compact return nil if versions.empty? numbers = versions.map { |x| x.to_i } vers = numbers.max if vers == 20 if numbers.size > 1 # Don't return 20 (L) by default, as it's not yet stable. numbers.delete(vers) vers = numbers.max else vers = 'L' end end vers.to_s end end def api_version @api_version ||= latest_api_version end def target_api_version @target_api_version ||= latest_api_version end def versionized_build_dir File.join(build_dir, build_mode_name + '-' + api_version) end def build_tools_dir @build_tools_dir ||= Dir.glob(sdk_path + '/build-tools/*').sort { |x, y| File.basename(x) <=> File.basename(y) }.max end def apk_path File.join(versionized_build_dir, name + '.apk') end def ndk_toolchain_bin_dir @ndk_toolchain_bin_dir ||= begin paths = ['3.3', '3.4', '3.5'].map do |x| File.join(ndk_path, "toolchains/llvm-#{x}/prebuilt/darwin-x86_64/bin") end path = paths.find { |x| File.exist?(x) } App.fail "Can't locate a proper NDK toolchain (paths tried: #{paths.join(' ')})" unless path path end end def cc File.join(ndk_toolchain_bin_dir, 'clang') end def cxx File.join(ndk_toolchain_bin_dir, 'clang++') end def asflags archflags = case arch when 'armv5te' "-march=armv5te" when 'armv7' "-march=armv7a -mfpu=vfpv3-d16" else raise "Invalid arch `#{arch}'" end "-no-canonical-prefixes -target #{arch}-none-linux-androideabi #{archflags} -mthumb -msoft-float -marm -gcc-toolchain \"#{ndk_path}/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64\"" end def api_version_ndk @api_version_ndk ||= # NDK does not provide headers for versions of Android with no native # API changes (ex. 10 and 11 are the same as 9). case api_version when '6', '7' '5' when '10', '11' '9' else api_version end end def cflags archflags = case arch when 'armv5te' "-mtune=xscale" end "#{asflags} #{archflags} -MMD -MP -fpic -ffunction-sections -funwind-tables -fexceptions -fstack-protector -fno-rtti -fno-strict-aliasing -O0 -g3 -fno-omit-frame-pointer -DANDROID -I\"#{ndk_path}/platforms/android-#{api_version_ndk}/arch-arm/usr/include\" -Wformat -Werror=format-security" end def cxxflags "#{cflags} -I\"#{ndk_path}/sources/cxx-stl/stlport/stlport\"" end def payload_library_filename "lib#{payload_library_name}.so" end def payload_library_name 'payload' end def ldflags "-Wl,-soname,#{payload_library_filename} -shared --sysroot=\"#{ndk_path}/platforms/android-#{api_version_ndk}/arch-arm\" -lgcc -gcc-toolchain \"#{ndk_path}/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64\" -no-canonical-prefixes -target #{arch}-none-linux-androideabi -Wl,--no-undefined -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now" end def versioned_datadir "#{motiondir}/data/android/#{api_version}" end def versioned_arch_datadir "#{versioned_datadir}/#{arch}" end def ldlibs # The order of the libraries matters here. "-L\"#{ndk_path}/platforms/android-#{api_version}/arch-arm/usr/lib\" -lstdc++ -lc -lm -llog -L\"#{versioned_arch_datadir}\" -lrubymotion-static -L#{ndk_path}/sources/cxx-stl/stlport/libs/armeabi -lstlport_static" end def armeabi_directory_name case arch when 'armv5te' 'armeabi' when 'armv7' 'armeabi-v7a' else raise "Invalid arch `#{arch}'" end end def bin_exec(name) File.join(motiondir, 'bin', name) end def kernel_path File.join(versioned_arch_datadir, "kernel-#{arch}.bc") end def clean_project super vendored_bs_files(false).each do |path| if File.exist?(path) App.info 'Delete', path FileUtils.rm_f path end end end attr_reader :vendored_projects def vendor_project(opt) jar = opt.delete(:jar) App.fail "Expected `:jar' key/value pair in `#{opt}'" unless jar res = opt.delete(:resources) manifest = opt.delete(:manifest) App.fail "Expected `:manifest' key/value pair when `:resources' is given" if res and !manifest App.fail "Expected `:resources' key/value pair when `:manifest' is given" if manifest and !res App.fail "Unused arguments: `#{opt}'" unless opt.empty? package = nil if manifest line = `/usr/bin/xmllint --xpath '/manifest/@package' \"#{manifest}\"`.strip App.fail "Given manifest `#{manifest}' does not have a `package' attribute in the top-level element" if $?.to_i != 0 package = line.match(/package=\"(.+)\"$/)[1] end @vendored_projects << { :jar => jar, :resources => res, :manifest => manifest, :package => package } end def vendored_bs_files(create=true) @vendored_bs_files ||= begin vendored_projects.map do |proj| jar_file = proj[:jar] bs_file = File.join(File.dirname(jar_file), File.basename(jar_file) + '.bridgesupport') if create and (!File.exist?(bs_file) or File.mtime(jar_file) > File.mtime(bs_file)) App.info 'Create', bs_file sh "#{bin_exec('android/gen_bridge_metadata')} -o \"#{bs_file}\" \"#{jar_file}\"" end bs_file end end end def logs_components @logs_components ||= begin ary = [] ary << package_path + ':I' %w{AndroidRuntime chromium dalvikvm Bundle art}.each do |comp| ary << comp + ':E' end ary end end attr_reader :manifest_entries def manifest_entry(toplevel_element=nil, element, attributes) if toplevel_element App.fail "toplevel element must be either nil or `application'" unless toplevel_element == 'application' end elems = (@manifest_entries[toplevel_element] ||= []) elems << { :name => element, :attributes => attributes } end def manifest_xml_lines(toplevel_element) (@manifest_entries[toplevel_element] or []).map do |elem| name = elem[:name] attributes = elem[:attributes] attributes_line = attributes.to_a.map do |key, val| key = case key when :name 'android:name' when :value 'android:value' else key end "#{key}=\"#{val}\"" end.join(' ') "<#{name} #{attributes_line}/>" end end attr_reader :release_keystore_path, :release_keystore_alias def release_keystore(path, alias_name) @release_keystore_path = path @release_keystore_alias = alias_name end def version(code, name) @version_code = code @version_name = name end end end; end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,108 @@ #!/usr/bin/ruby # encoding: utf-8 require 'fileutils' require 'pathname' require 'optparse' $stderr.reopen("/dev/null", "w") def gen_bridge_metadata(file) file = File.expand_path(file) case ext = File.extname(file) when '.jar' class_path = file classes = `/usr/bin/jar -tf #{file} | grep .class`.split("\n") when '.class' class_path = File.dirname(file) classes = [File.basename(file)] else die "Invalid file extension '#{ext}'. Supported extensions are 'jar' and 'class'" end # Decompile classes. classes.map! { |x| x.gsub('/', '.').gsub('$', '.').sub(/\.class$/, '') } txt = `/usr/bin/javap -classpath "#{class_path}" -s #{classes.join(' ')}` # Create the BridgeSupport text. res = txt.scan(/(class)\s+([^\s{]+)[^{]*\{([^}]+)\}/) # Grab classes res += txt.scan(/(interface)\s+([^\s{]+)[^{]*\{([^}]+)\}/) # Also grab interfaces res.each do |type, elem, body_txt| elem_path = encode_xml(elem.gsub(/\./, '/')) @bs_data << <<EOS <#{type} name=\"#{elem_path}\"> EOS body_txt.strip.split(/\n/).each_cons(2) do |elem_line, signature_line| signature_line = encode_xml(signature_line.strip) md = signature_line.match(/^(?:Signature|descriptor):\s+(.+)$/) next unless md signature = md[1] elem_line = encode_xml(elem_line.strip) # puts elem_line if md = elem_line.match(/([^(\s]+)\(/) # A method. method = md[1] if method == elem # An initializer. method = '<init>' end class_method = elem_line.include?('static') @bs_data << <<EOS <method name=\"#{method}\" type=\"#{signature}\"#{class_method ? ' class_method="true"' : ''}/> EOS # elsif md = elem_line.match(/\s([^;\s]+);$/) # # A constant. # constant = md[1] # @bs_data << <<EOS # <const name=\"#{constant}\" type=\"#{signature}\"/> # EOS end end @bs_data << <<EOS </#{type}> EOS end end def encode_xml(string) string.gsub(/[&<>"]/, ">" => ">", "<" => "<", "&" => "&", '"' => """) end def die(*msg) $stderr.puts msg exit 1 end if __FILE__ == $0 OptionParser.new do |opts| opts.banner = "Usage: #{File.basename(__FILE__)} [options] [<jar-file>|<class-file>...]" opts.separator '' opts.separator 'Options:' opts.on('-o', '--output FILE', 'Write output to the given file.') do |opt| die 'Output file can\'t be specified more than once' if @out_file @out_file = opt end if ARGV.empty? die opts.banner else opts.parse!(ARGV) @bs_data = '' @bs_data << <<EOS <?xml version='1.0'?> <signatures version='1.0'> EOS ARGV.each { |file| gen_bridge_metadata(file) } @bs_data << <<EOS </signatures> EOS File.open(@out_file, 'w') { |io| io.write(@bs_data) } end end end