Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion ruby/private/binary.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,17 @@ Supports `$(location)` expansion for targets from `srcs`, `data` and `deps`.
allow_single_file = True,
default = "@rules_ruby//ruby/private:coverage.rb",
),
"_runfiles_helper": attr.label(
allow_single_file = True,
default = "@rules_ruby//ruby/runfiles:lib/bazel/runfiles.rb",
),
"coverage_filters": attr.string_list(
doc = "Additional coverage filters to add to SimpleCov. Only applied during 'bazel coverage'.",
),
}

# buildifier: disable=function-docstring
def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, java_bin = "", jars_home_strip_suffix = "", coverage_helper = "", is_jruby = False):
def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {}, java_bin = "", jars_home_strip_suffix = "", coverage_helper = "", runfiles_helper = "", is_jruby = False):
toolchain = ctx.toolchains["@rules_ruby//ruby:toolchain_type"]
if ctx.attr.ruby != None:
toolchain = ctx.attr.ruby[platform_common.ToolchainInfo]
Expand Down Expand Up @@ -129,6 +133,7 @@ def generate_rb_binary_script(ctx, binary, bundler = False, args = [], env = {},
"{rlocation_function}": rlocation_function,
"{locate_binary_in_runfiles}": locate_binary_in_runfiles,
"{coverage_helper}": coverage_helper,
"{runfiles_helper}": runfiles_helper,
"{is_jruby}": "true" if is_jruby else "",
},
)
Expand All @@ -153,6 +158,7 @@ def rb_binary_impl(ctx):
ruby_toolchain = ctx.attr.ruby[platform_common.ToolchainInfo]
tools = list(ruby_toolchain.files)
tools.append(ctx.file._coverage_helper)
tools.append(ctx.file._runfiles_helper)

if ruby_toolchain.version.startswith("jruby"):
java_toolchain = ctx.toolchains["@bazel_tools//tools/jdk:runtime_toolchain_type"]
Expand Down Expand Up @@ -220,6 +226,7 @@ def rb_binary_impl(ctx):
java_bin = java_bin,
jars_home_strip_suffix = jars_home_strip_suffix,
coverage_helper = _to_rlocation_path(ctx, ctx.file._coverage_helper),
runfiles_helper = _to_rlocation_path(ctx, ctx.file._runfiles_helper),
is_jruby = ruby_toolchain.version.startswith("jruby"),
)

Expand Down
4 changes: 4 additions & 0 deletions ruby/private/binary/binary.cmd.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ set RUNFILES_MANIFEST_ONLY=1
call :rlocation {ruby} ruby
for %%a in ("!ruby!\..") do set PATH=%%~fa;%PATH%

:: Set RUBYLIB to include the runfiles library.
call :rlocation {runfiles_helper} _runfiles_helper_path
for %%a in ("!_runfiles_helper_path!\..\..") do set "RUBYLIB=%%~fa;!RUBYLIB!"

:: Find location of JAVA_HOME in runfiles.
if "{java_bin}" neq "" (
call :rlocation {java_bin} java_bin
Expand Down
3 changes: 3 additions & 0 deletions ruby/private/binary/binary.sh.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ realpath() (

export RUNFILES_DIR="$(realpath "${RUNFILES_DIR:-$0.runfiles}")"

# Set RUBYLIB to include the runfiles library.
export RUBYLIB="$(dirname "$(dirname "$(rlocation "{runfiles_helper}")")")${RUBYLIB:+:${RUBYLIB}}"

# Find location of Ruby in runfiles.
export PATH=$(dirname $(rlocation {ruby})):$PATH

Expand Down
16 changes: 16 additions & 0 deletions ruby/runfiles/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//ruby:defs.bzl", "rb_library", "rb_test")

exports_files(["lib/bazel/runfiles.rb"])

rb_library(
name = "runfiles",
srcs = ["lib/bazel/runfiles.rb"],
visibility = ["//visibility:public"],
)

rb_test(
name = "runfiles_test",
size = "small",
srcs = ["runfiles_test.rb"],
tags = ["manual"],
)
107 changes: 107 additions & 0 deletions ruby/runfiles/lib/bazel/runfiles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require 'pathname'

module Bazel
# Resolves runtime paths to data dependencies using either a
# manifest file or a runfiles directory.
class Runfiles
def self.create(env = ENV)
manifest_file = env['RUNFILES_MANIFEST_FILE']
runfiles_dir = env['RUNFILES_DIR']

return new(ManifestBased.new(manifest_file)) if manifest_file && !manifest_file.empty?
return new(DirectoryBased.new(runfiles_dir)) if runfiles_dir && !runfiles_dir.empty?

create_from_program_name($PROGRAM_NAME)
end

def self.create_from_program_name(program_name)
if File.exist?("#{program_name}.runfiles_manifest")
new(ManifestBased.new("#{program_name}.runfiles_manifest"))
elsif File.exist?("#{program_name}.runfiles")
new(DirectoryBased.new("#{program_name}.runfiles"))
else
new(DirectoryBased.new(''))
end
end

def initialize(strategy)
@strategy = strategy
end

def rlocation(path)
raise ArgumentError, 'path must not be empty' if path.to_s.empty?

invalid_path = %r{\A\.\.[/\\]|[/\\]\.\.[/\\]|\A\.[/\\]|[/\\]\.[/\\]|[/\\]\.\z|[/\\][/\\]}
raise ArgumentError, "path is not valid: #{path.inspect}" if path.match?(invalid_path)

return path if Pathname.new(path).absolute?

@strategy.rlocation(path)
end

# Resolves paths by looking them up in a runfiles MANIFEST file.
class ManifestBased
def initialize(manifest_path)
@entries = parse_manifest(manifest_path)
end

def rlocation(path)
return @entries[path] if @entries.key?(path)

prefix = File.dirname(path)
while prefix != '.' && prefix != '/'
base = @entries[prefix]
return "#{base}#{path[prefix.length..]}" if base && !base.empty?

prefix = File.dirname(prefix)
end

nil
end

private

def parse_manifest(path)
entries = {}
return entries unless File.exist?(path)

File.foreach(path) do |line|
line.chomp!
next if line.empty?

key, value = parse_entry(line)
entries[key] = value
end

entries
end

def parse_entry(line)
escaped = line.delete_prefix!(' ')
key, _, value = line.partition(' ')
return [key, value] unless escaped

[unescape(key), unescape(value)]
end

def unescape(str)
str.gsub(/\\[snb]/, '\s' => ' ', '\n' => "\n", '\b' => '\\')
end
end

# Resolves paths by joining them onto a runfiles directory root.
class DirectoryBased
def initialize(runfiles_dir)
@runfiles_dir = runfiles_dir
end

def rlocation(path)
return nil if @runfiles_dir.empty?

File.join(@runfiles_dir, path)
end
end
end
end
139 changes: 139 additions & 0 deletions ruby/runfiles/runfiles_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# frozen_string_literal: true

require 'minitest/autorun'
require 'tmpdir'
require 'bazel/runfiles'

class ManifestBasedTest < Minitest::Test
def setup
@dir = Dir.mktmpdir
@manifest = File.join(@dir, 'MANIFEST')
end

def teardown
FileUtils.rm_rf(@dir)
end

def strategy(contents)
File.write(@manifest, contents)
Bazel::Runfiles::ManifestBased.new(@manifest)
end

def test_exact_match
r = strategy("_main/pkg/file.txt /abs/pkg/file.txt\n")
assert_equal '/abs/pkg/file.txt', r.rlocation('_main/pkg/file.txt')
end

def test_prefix_walk
r = strategy("_main/pkg /abs/pkg\n")
assert_equal '/abs/pkg/sub/file.txt', r.rlocation('_main/pkg/sub/file.txt')
end

def test_missing_path_returns_nil
r = strategy("_main/pkg/file.txt /abs/pkg/file.txt\n")
assert_nil r.rlocation('_main/pkg/other.txt')
end

def test_empty_file_convention
r = strategy("_main/empty.txt\n")
assert_equal '', r.rlocation('_main/empty.txt')
end

def test_escaped_path_with_spaces
r = strategy(" _main/dir\\swith\\sspaces/file.txt /abs/dir\\swith\\sspaces/file.txt\n")
assert_equal '/abs/dir with spaces/file.txt', r.rlocation('_main/dir with spaces/file.txt')
end

def test_escaped_path_with_backslash_and_newline
r = strategy(" _main/a\\bs\\nb /abs/a\\bs\\nb\n")
assert_equal "/abs/a\\s\nb", r.rlocation("_main/a\\s\nb")
end

def test_escaped_empty_file
r = strategy(" _main/weird\\spath\n")
assert_equal '', r.rlocation('_main/weird path')
end

def test_prefix_with_empty_value_not_matched_as_directory
r = strategy("_main/empty.txt \n_main/empty.txt/child.rb /abs/child.rb\n")
assert_equal '/abs/child.rb', r.rlocation('_main/empty.txt/child.rb')
end
end

class DirectoryBasedTest < Minitest::Test
def setup
@dir = Dir.mktmpdir
end

def teardown
FileUtils.rm_rf(@dir)
end

def test_rlocation_joins_dir_and_path
r = Bazel::Runfiles::DirectoryBased.new(@dir)
assert_equal File.join(@dir, '_main/pkg/file.txt'), r.rlocation('_main/pkg/file.txt')
end

def test_rlocation_returns_nil_when_no_dir
r = Bazel::Runfiles::DirectoryBased.new('')
assert_nil r.rlocation('_main/pkg/file.txt')
end
end

class RunfilesTest < Minitest::Test
def setup
@dir = Dir.mktmpdir
@manifest = File.join(@dir, 'MANIFEST')
File.write(@manifest, "_main/a.txt /abs/a.txt\n")
end

def teardown
FileUtils.rm_rf(@dir)
end

def directory_runfiles(dir = @dir)
Bazel::Runfiles.new(Bazel::Runfiles::DirectoryBased.new(dir))
end

def test_empty_path_raises
assert_raises(ArgumentError) { directory_runfiles.rlocation('') }
end

def test_nil_path_raises
assert_raises(ArgumentError) { directory_runfiles.rlocation(nil) }
end

def test_absolute_path_returned_unchanged
assert_equal '/already/absolute.txt', directory_runfiles.rlocation('/already/absolute.txt')
end

def test_non_normalized_paths_raise
['../foo', 'foo/../bar', './foo', 'foo/./bar', 'foo/.', 'foo//bar'].each do |path|
assert_raises(ArgumentError, "expected #{path.inspect} to raise") do
directory_runfiles.rlocation(path)
end
end
end

def test_absolute_without_drive_letter_raises
assert_raises(ArgumentError) { directory_runfiles.rlocation('\\foo') }
end

def test_create_picks_manifest_mode_when_manifest_file_set
r = Bazel::Runfiles.create('RUNFILES_MANIFEST_FILE' => @manifest)
assert_equal '/abs/a.txt', r.rlocation('_main/a.txt')
end

def test_create_picks_directory_mode_when_dir_set
r = Bazel::Runfiles.create('RUNFILES_DIR' => @dir)
assert_equal File.join(@dir, '_main/a.txt'), r.rlocation('_main/a.txt')
end

def test_create_prefers_manifest_over_directory
r = Bazel::Runfiles.create(
'RUNFILES_MANIFEST_FILE' => @manifest,
'RUNFILES_DIR' => @dir
)
assert_equal '/abs/a.txt', r.rlocation('_main/a.txt')
end
end