Created
December 25, 2014 04:46
-
-
Save bbtfr/204a1c61182027f17081 to your computer and use it in GitHub Desktop.
RubyMotion Patch
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 characters
| # 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 characters
| # 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 characters
| #!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For those looking to add activity and intent-filter to the manifest.xml with ruby you can do: