Add rules for packaging up Ruby code.

These were originally written by Parker and then cleaned up by me.

Change-Id: I93b66c83f80ca86a267abb84cab5f160b837083b
diff --git a/tools/build_rules/ruby.bzl b/tools/build_rules/ruby.bzl
new file mode 100644
index 0000000..7c02745
--- /dev/null
+++ b/tools/build_rules/ruby.bzl
@@ -0,0 +1,93 @@
+ZIP_PATH = '/usr/bin/zip'
+
+ruby_file_types = FileType(['.rb'])
+
+def _collect_transitive_sources(ctx):
+  source_files = set(order='compile')
+  for dep in ctx.attr.deps:
+    source_files += dep.transitive_ruby_files
+
+  source_files += ruby_file_types.filter(ctx.files.srcs)
+  return source_files
+
+def _ruby_library_impl(ctx):
+  transitive_sources = _collect_transitive_sources(ctx)
+  return struct(
+    transitive_ruby_files = transitive_sources,
+  )
+
+def _ruby_binary_impl(ctx):
+  main_file = ctx.files.srcs[0]
+  transitive_sources = _collect_transitive_sources(ctx)
+  executable = ctx.outputs.executable
+  manifest_file = ctx.outputs.manifest
+
+  path_map = '\n'.join([('%s|%s' % (item.short_path, item.path))
+                        for item in transitive_sources])
+  ctx.file_action(output=manifest_file, content=path_map)
+
+  ctx.action(
+    inputs = list(transitive_sources) + [manifest_file],
+    outputs = [ executable ],
+    arguments = [ manifest_file.path, executable.path, main_file.path ],
+    executable = ctx.executable._ruby_linker,
+  )
+
+  return struct(
+    files = set([executable]),
+    runfiles = ctx.runfiles(collect_data = True),
+  )
+
+
+_ruby_attrs = {
+  'srcs': attr.label_list(
+    allow_files = ruby_file_types,
+    mandatory = True,
+    non_empty = True,
+  ),
+  'deps': attr.label_list(
+    providers = ['transitive_ruby_files'],
+    allow_files = False,
+  ),
+  'data': attr.label_list(
+    allow_files = True,
+    cfg = DATA_CFG,
+  ),
+}
+
+'''Packages ruby code into a library.
+
+The files can use require with paths from the base of the workspace and/or
+require_relative with other files that are part of the library.
+require also works from the filesystem as usual.
+
+Attrs:
+  srcs: Ruby source files to include.
+  deps: Other ruby_library rules to include.
+'''
+ruby_library = rule(
+  implementation = _ruby_library_impl,
+  attrs = _ruby_attrs,
+)
+
+'''Packages ruby code into a binary which can be run.
+
+See ruby_library for details on how require works.
+
+Attrs:
+  srcs: Ruby source files to include. The first one is loaded to at startup.
+  deps: ruby_library rules to include.
+'''
+ruby_binary = rule(
+  implementation = _ruby_binary_impl,
+  executable = True,
+  attrs = _ruby_attrs + {
+    '_ruby_linker': attr.label(
+      executable = True,
+      default = Label('//tools/ruby:standalone_ruby'),
+    )
+  },
+  outputs = {
+    'manifest': '%{name}.tar_manifest',
+  },
+)
diff --git a/tools/ruby/BUILD b/tools/ruby/BUILD
new file mode 100644
index 0000000..426a9f1
--- /dev/null
+++ b/tools/ruby/BUILD
@@ -0,0 +1,5 @@
+py_binary(
+  name = 'standalone_ruby',
+  srcs = ['standalone_ruby.py'],
+  visibility = ['//visibility:public'],
+)
diff --git a/tools/ruby/standalone_ruby.py b/tools/ruby/standalone_ruby.py
new file mode 100644
index 0000000..0c574e0
--- /dev/null
+++ b/tools/ruby/standalone_ruby.py
@@ -0,0 +1,75 @@
+#!/usr/bin/python
+
+import zlib, sys, os, stat
+
+with file(sys.argv[2], "wb+") as outf:
+  outf.write("""#!/usr/bin/env ruby
+require "zlib"
+
+def zip_file_binding()
+  binding()
+end
+
+module Kernel
+  class ZipRequireRecord
+    attr_accessor :fname, :val
+    def initialize(fname, val)
+      @fname = fname
+      @val = val
+    end
+    def do_require()
+      return false if !@val
+      eval(@val, binding = zip_file_binding(), filename="//@bazel/" + @fname)
+      @val = nil
+      return true
+    end
+  end
+  BAZEL_START_ENTRY = "/@bazel/"
+  ZIP_TEMP_OBJS = {}
+  def require_relative(relative_feature)
+    file = caller.first.split(/:\d/,2).first
+    raise LoadError, "require_relative is called in #{$1}" if /\A\((.*)\)/ =~ file
+    require File.expand_path(relative_feature, File.dirname(file))
+  end
+  def require_zip(moduleName)
+    if moduleName.start_with?(BAZEL_START_ENTRY)
+      moduleName = moduleName[BAZEL_START_ENTRY.length..-1]
+    end
+    zip_obj = ZIP_TEMP_OBJS[moduleName]
+    if not zip_obj and !moduleName.end_with?('.rb')
+      zip_obj = ZIP_TEMP_OBJS[moduleName + '.rb']
+    end
+    return zip_obj.do_require() if (zip_obj)
+    return pre_zip_load_require(moduleName)
+  end
+  alias :pre_zip_load_require :require
+  alias :require :require_zip
+  def init_app(main_fname)
+    $0 = "//@bazel/" + main_fname
+    Kernel.require_zip(main_fname)
+  end
+  def register_inline_file(fname, data)
+    raise if ZIP_TEMP_OBJS.has_key?(fname)
+    ZIP_TEMP_OBJS[fname] = Kernel::ZipRequireRecord.new(fname, data.force_encoding("UTF-8"))
+  end
+end
+
+my_data = Zlib.inflate(DATA.read())
+""")
+
+  blob_data = []
+  offset = 0
+  with file(sys.argv[1], 'r') as infile:
+    for line in infile.xreadlines():
+      file_outname, file_binname = line.strip().split('|')
+      with file(file_binname, 'r') as obj_file:
+        obj = obj_file.read()
+      blob_data.append(obj)
+      new_off = offset + len(obj)
+      outf.write("Kernel.register_inline_file(%s, my_data.slice(%s...%s))\n" %
+                 (repr(file_outname), offset, new_off))
+      offset = new_off
+
+  outf.write("Kernel.init_app(%s)\n" % repr(sys.argv[3]))
+  outf.write("__END__\n")
+  outf.write(zlib.compress("".join(blob_data)))