1class MRuby::Toolchain::Android
2
3  DEFAULT_ARCH = 'armeabi' # TODO : Revise if arch should have a default
4
5  DEFAULT_TOOLCHAIN = :clang
6
7  DEFAULT_NDK_HOMES = %w{
8    /usr/local/opt/android-sdk/ndk-bundle
9    /usr/local/opt/android-ndk
10    ~/Android/Sdk/ndk-bundle
11    %LOCALAPPDATA%/Android/android-sdk/ndk-bundle
12    %LOCALAPPDATA%/Android/android-ndk
13    ~/Library/Android/sdk/ndk-bundle
14    ~/Library/Android/ndk
15  }
16
17  TOOLCHAINS = [:clang, :gcc]
18
19  ARCHITECTURES = %w{
20    armeabi armeabi-v7a arm64-v8a
21    x86 x86_64
22    mips mips64
23  }
24
25  class AndroidNDKHomeNotFound < StandardError
26    def message
27        <<-EOM
28Couldn't find Android NDK Home.
29Set ANDROID_NDK_HOME environment variable or set :ndk_home parameter
30        EOM
31    end
32  end
33
34  class PlatformDirNotFound < StandardError
35    def message
36        <<-EOM
37Couldn't find Android NDK platform directories.
38Set ANDROID_PLATFORM environment variable or set :platform parameter
39        EOM
40    end
41  end
42
43  attr_reader :params
44
45  def initialize(params)
46    @params = params
47  end
48
49  def bin_gcc(command)
50    command = command.to_s
51
52    command = case arch
53      when /armeabi/    then 'arm-linux-androideabi-'
54      when /arm64-v8a/  then 'aarch64-linux-android-'
55      when /x86_64/     then 'x86_64-linux-android-'
56      when /x86/        then 'i686-linux-android-'
57      when /mips64/     then 'mips64el-linux-android-'
58      when /mips/       then 'mipsel-linux-android-'
59      end + command
60
61    gcc_toolchain_path.join('bin', command).to_s
62  end
63
64  def bin(command)
65    command = command.to_s
66    toolchain_path.join('bin', command).to_s
67  end
68
69  def home_path
70    @home_path ||= Pathname(
71      params[:ndk_home] ||
72      ENV['ANDROID_NDK_HOME'] ||
73      DEFAULT_NDK_HOMES.find { |path|
74        path.gsub! '%LOCALAPPDATA%', ENV['LOCALAPPDATA'] || '%LOCALAPPDATA%'
75        path.gsub! '\\', '/'
76        path.gsub! '~', Dir.home || '~'
77        File.directory?(path)
78      } || raise(AndroidNDKHomeNotFound)
79    )
80  end
81
82  def toolchain
83    @toolchain ||= params.fetch(:toolchain){ DEFAULT_TOOLCHAIN }
84  end
85
86  def toolchain_path
87    @toolchain_path ||= case toolchain
88      when :gcc
89        gcc_toolchain_path
90      when :clang
91        home_path.join('toolchains', 'llvm' , 'prebuilt', host_platform)
92      end
93  end
94
95  def gcc_toolchain_path
96    if @gcc_toolchain_path === nil then
97      prefix = case arch
98        when /armeabi/    then 'arm-linux-androideabi-'
99        when /arm64-v8a/  then 'aarch64-linux-android-'
100        when /x86_64/     then 'x86_64-'
101        when /x86/        then 'x86-'
102        when /mips64/     then 'mips64el-linux-android-'
103        when /mips/       then 'mipsel-linux-android-'
104        end
105
106      test = case arch
107        when /armeabi/    then 'arm-linux-androideabi-*'
108        when /arm64-v8a/  then 'aarch64-linux-android-*'
109        when /x86_64/     then 'x86_64-*'
110        when /x86/        then 'x86-*'
111        when /mips64/     then 'mips64el-linux-android-*'
112        when /mips/       then 'mipsel-linux-android-*'
113        end
114
115      gcc_toolchain_version = Dir[home_path.join('toolchains', test)].map{|t| t.match(/-(\d+\.\d+)$/); $1.to_f }.max
116      @gcc_toolchain_path = home_path.join('toolchains', prefix + gcc_toolchain_version.to_s, 'prebuilt', host_platform)
117    end
118    @gcc_toolchain_path
119  end
120
121  def host_platform
122    @host_platform ||= case RUBY_PLATFORM
123      when /cygwin|mswin|mingw|bccwin|wince|emx/i
124        path = home_path.join('toolchains', 'llvm' , 'prebuilt', 'windows*')
125        Dir.glob(path.to_s){ |item|
126          next if File.file?(item)
127          path = Pathname(item)
128          break
129        }
130        path.basename
131      when /x86_64-darwin/i
132        'darwin-x86_64'
133      when /darwin/i
134        'darwin-x86'
135      when /x86_64-linux/i
136        'linux-x86_64'
137      when /linux/i
138        'linux-x86'
139      else
140        raise NotImplementedError, "Unknown host platform (#{RUBY_PLATFORM})"
141      end
142  end
143
144  def arch
145    @arch ||= (params[:arch] || ENV['ANDROID_ARCH'] || DEFAULT_ARCH).to_s
146  end
147
148  def sysroot
149    @sysroot ||= home_path.join('platforms', platform,
150        case arch
151        when /armeabi/    then 'arch-arm'
152        when /arm64-v8a/  then 'arch-arm64'
153        when /x86_64/     then 'arch-x86_64'
154        when /x86/        then 'arch-x86'
155        when /mips64/     then 'arch-mips64'
156        when /mips/       then 'arch-mips'
157        end
158      ).to_s
159  end
160
161  def platform
162    if @platform === nil then
163      @platform = params[:platform] || ENV['ANDROID_PLATFORM'] || nil
164      if @platform === nil
165        Dir.glob(home_path.join('platforms/android-*').to_s){ |item|
166          next if File.file?(item)
167          if @platform === nil
168            @platform = Integer(item.rpartition('-')[2])
169          else
170            platform = Integer(item.rpartition('-')[2])
171            @platform = platform > @platform ? platform : @platform
172          end
173        }
174        if @platform === nil
175          raise(PlatformDirNotFound)
176        else
177          @platform = "android-#{@platform}"
178        end
179      end
180    end
181    if Integer(@platform.rpartition('-')[2]) < 21
182      case arch
183      when /arm64-v8a/, /x86_64/, /mips64/
184        raise NotImplementedError, "Platform (#{@platform}) has no implementation for architecture (#{arch})"
185      end
186    end
187    @platform
188  end
189
190  def armeabi_v7a_mfpu
191    @armeabi_v7a_mfpu ||= (params[:mfpu] || 'vfpv3-d16').to_s
192  end
193
194  def armeabi_v7a_mfloat_abi
195    @armeabi_v7a_mfloat_abi ||= (params[:mfloat_abi] || 'softfp').to_s
196  end
197
198  def no_warn_mismatch
199    if %W(soft softfp).include? armeabi_v7a_mfloat_abi
200      ''
201    else
202      ',--no-warn-mismatch'
203    end
204  end
205
206  def cc
207    case toolchain
208    when :gcc then bin_gcc('gcc')
209    when :clang then bin('clang')
210    end
211  end
212
213  def ar
214    case toolchain
215    when :gcc   then bin_gcc('ar')
216    when :clang then bin_gcc('ar')
217    end
218  end
219
220  def ctarget
221    flags = []
222
223    case toolchain
224    when :gcc
225      case arch
226      when /armeabi-v7a/  then flags += %W(-march=armv7-a)
227      when /armeabi/      then flags += %W(-march=armv5te)
228      when /arm64-v8a/    then flags += %W(-march=armv8-a)
229      when /x86_64/       then flags += %W(-march=x86-64)
230      when /x86/          then flags += %W(-march=i686)
231      when /mips64/       then flags += %W(-march=mips64r6)
232      when /mips/         then flags += %W(-march=mips32)
233      end
234    when :clang
235      case arch
236      when /armeabi-v7a/  then flags += %W(-target armv7-none-linux-androideabi)
237      when /armeabi/      then flags += %W(-target armv5te-none-linux-androideabi)
238      when /arm64-v8a/    then flags += %W(-target aarch64-none-linux-android)
239      when /x86_64/       then flags += %W(-target x86_64-none-linux-android)
240      when /x86/          then flags += %W(-target i686-none-linux-android)
241      when /mips64/       then flags += %W(-target mips64el-none-linux-android)
242      when /mips/         then flags += %W(-target mipsel-none-linux-android)
243      end
244    end
245
246    case arch
247    when /armeabi-v7a/  then flags += %W(-mfpu=#{armeabi_v7a_mfpu} -mfloat-abi=#{armeabi_v7a_mfloat_abi})
248    when /armeabi/      then flags += %W(-mtune=xscale -msoft-float)
249    when /arm64-v8a/    then flags += %W()
250    when /x86_64/       then flags += %W()
251    when /x86/          then flags += %W()
252    when /mips64/       then flags += %W(-fmessage-length=0)
253    when /mips/         then flags += %W(-fmessage-length=0)
254    end
255
256    flags
257  end
258
259  def cflags
260    flags = []
261
262    flags += %W(-MMD -MP -D__android__ -DANDROID --sysroot="#{sysroot}")
263    flags += ctarget
264    case toolchain
265    when :gcc
266    when :clang
267      flags += %W(-gcc-toolchain "#{gcc_toolchain_path}" -Wno-invalid-command-line-argument -Wno-unused-command-line-argument)
268    end
269    flags += %W(-fpic -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes)
270
271    flags
272  end
273
274  def ldflags
275    flags = []
276
277    flags += %W(--sysroot="#{sysroot}")
278
279    flags
280  end
281
282  def ldflags_before_libraries
283    flags = []
284
285    case toolchain
286    when :gcc
287      case arch
288      when /armeabi-v7a/  then flags += %W(-Wl#{no_warn_mismatch})
289      end
290    when :clang
291      flags += %W(-gcc-toolchain "#{gcc_toolchain_path.to_s}")
292      case arch
293      when /armeabi-v7a/  then flags += %W(-target armv7-none-linux-androideabi -Wl,--fix-cortex-a8#{no_warn_mismatch})
294      when /armeabi/      then flags += %W(-target armv5te-none-linux-androideabi)
295      when /arm64-v8a/    then flags += %W(-target aarch64-none-linux-android)
296      when /x86_64/       then flags += %W(-target x86_64-none-linux-android)
297      when /x86/          then flags += %W(-target i686-none-linux-android)
298      when /mips64/       then flags += %W(-target mips64el-none-linux-android)
299      when /mips/         then flags += %W(-target mipsel-none-linux-android)
300      end
301    end
302    flags += %W(-no-canonical-prefixes)
303
304    flags
305  end
306end
307
308MRuby::Toolchain.new(:android) do |conf, params|
309  android = MRuby::Toolchain::Android.new(params)
310
311  toolchain android.toolchain
312
313  [conf.cc, conf.cxx, conf.objc, conf.asm].each do |cc|
314    cc.command = android.cc
315    cc.flags = android.cflags
316  end
317
318  conf.archiver.command = android.ar
319  conf.linker.command = android.cc
320  conf.linker.flags = android.ldflags
321  conf.linker.flags_before_libraries = android.ldflags_before_libraries
322end
323