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.
RubyMotion Patch
# 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'
# 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
#!/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
@p8
Copy link

p8 commented Dec 10, 2020

For those looking to add activity and intent-filter to the manifest.xml with ruby you can do:

Motion::Project::App.setup do |app|
  ...
  app.manifest.child('application') do |application|
    application.add_child('activity') do |activity|
      activity['android:name'] = -> { app.main_activity }
      activity['android:label'] = -> { "#{app.name}" }
      activity.add_child('intent-filter') do |filter|
        filter.add_child('action', 'android:name' => 'android.intent.action.VIEW')
        filter.add_child('category', 'android:name' => 'android.intent.category.DEFAULT')
        filter.add_child('category', 'android:name' => 'android.intent.category.BROWSABLE')
        filter.add_child('data', 'android:scheme' => 'https', 'android:host' => 'example.com', 'android:pathPrefix' => "/")
      end
    end
  end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment