Skip to content

Instantly share code, notes, and snippets.

@bbtfr
Created December 25, 2014 04:46
Show Gist options
  • Select an option

  • Save bbtfr/204a1c61182027f17081 to your computer and use it in GitHub Desktop.

Select an option

Save bbtfr/204a1c61182027f17081 to your computer and use it in GitHub Desktop.

Revisions

  1. bbtfr created this gist Dec 25, 2014.
    614 changes: 614 additions & 0 deletions android.rb
    Original 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'

    343 changes: 343 additions & 0 deletions config.rb
    Original 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
    108 changes: 108 additions & 0 deletions gen_bridge_metadata
    Original 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 = '&lt;init&gt;'
    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(/[&<>"]/, ">" => "&gt;", "<" => "&lt;", "&" => "&amp;", '"' => "&quot;")
    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