class Autoproj::PackageManagers::BundlerManager

Package manager interface for the RubyGems system

Constants

GemEntry

Attributes

with_doc[RW]
with_prerelease[W]
cache_dir[RW]

Directory with cached .gem packages

The directory must exist, but may be empty. It is initialized with {BundlerManager.cache_dir}

@return [String]

Public Class Methods

add_build_configuration_for(gem_name, build_config, ws: Autoproj.workspace) click to toggle source

Add new build configuration arguments for a given gem

This is meant to be used from the Autoproj configuration files, e.g. overrides.rb or package configuration

# File lib/autoproj/package_managers/bundler_manager.rb, line 172
def self.add_build_configuration_for(gem_name, build_config, ws: Autoproj.workspace)
    c = ws.config.get("bundler.build", {})
    c[gem_name] = [c[gem_name], build_config].compact.join(" ")
    ws.config.set("bundler.build", c)
end
apply_build_config(ws) click to toggle source

@api private

Apply configured per-gem build configuration options

@param [Workspace] ws the workspace whose bundler configuration

should be updated

@return [void]

# File lib/autoproj/package_managers/bundler_manager.rb, line 205
def self.apply_build_config(ws)
    root_dir = File.join(ws.prefix_dir, "gems")
    current_config_path = File.join(root_dir, ".bundle", "config")
    current_config =
        if File.file?(current_config_path)
            File.readlines(current_config_path)
        else
            []
        end

    build_config = {}
    per_gem_build_config(ws).each do |name, conf|
        build_config[name.upcase] = conf
    end

    new_config = current_config.map do |line|
        next(line) unless (m = line.match(/BUNDLE_BUILD__(.*): "(.*)"$/))
        next unless (desired_config = build_config.delete(m[1]))

        if m[2] == desired_config
            line
        else
            "BUNDLE_BUILD__#{m[1]}: \"#{desired_config}\""
        end
    end.compact

    build_config.each do |name, config|
        new_config << "BUNDLE_BUILD__#{name}: \"#{config}\""
    end

    if new_config != current_config
        FileUtils.mkdir_p File.dirname(current_config_path)
        File.open(current_config_path, "w") do |io|
            io.write new_config.join
        end
    end
end
bundle_gem_path(ws, gem_name, bundler_version: ws.config.bundler_version, gem_home: nil, gemfile: nil) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 329
def self.bundle_gem_path(ws, gem_name,
    bundler_version: ws.config.bundler_version,
    gem_home: nil, gemfile: nil)
    path = String.new
    run_bundler(
        ws, "show", gem_name,
        bundler_version: bundler_version, gem_home: gem_home,
        gemfile: gemfile
    ) { |line| path << line }
    path.chomp
end
configure_build_for(gem_name, build_config, ws: Autoproj.workspace) click to toggle source

Set the build configuration for the given gem

This is meant to be used from the Autoproj configuration files, e.g. overrides.rb or package configuration

# File lib/autoproj/package_managers/bundler_manager.rb, line 182
def self.configure_build_for(gem_name, build_config, ws: Autoproj.workspace)
    c = ws.config.get("bundler.build", {})
    c[gem_name] = build_config
    ws.config.set("bundler.build", c)
end
default_bundler(ws) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 341
def self.default_bundler(ws)
    File.join(ws.dot_autoproj_dir, "bin", "bundle")
end
default_gemfile_path(ws) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 458
def self.default_gemfile_path(ws)
    File.join(ws.prefix_dir, "gems", "Gemfile")
end
per_gem_build_config(ws) click to toggle source

Enumerate the per-gem build configurations

# File lib/autoproj/package_managers/bundler_manager.rb, line 164
def self.per_gem_build_config(ws)
    ws.config.get("bundler.build", {})
end
remove_build_configuration_for(gem_name, ws: Autoproj.workspace) click to toggle source

Removes build configuration flags for the given gem

This is meant to be used from the Autoproj configuration files, e.g. overrides.rb or package configuration

# File lib/autoproj/package_managers/bundler_manager.rb, line 192
def self.remove_build_configuration_for(gem_name, ws: Autoproj.workspace)
    c = ws.config.get("bundler.build", {})
    c.delete(gem_name)
    ws.config.set("bundler.build", c)
end
rubylib_for_bundler() click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 619
def self.rubylib_for_bundler
    rx = Regexp.new("/gems/#{Regexp.quote("bundler-#{Bundler::VERSION}")}/")
    $LOAD_PATH.grep(rx).join(File::PATH_SEPARATOR)
end
run_bundler(ws, *commandline, bundler_version: ws.config.bundler_version, gem_home: ws.config.gems_gem_home, gemfile: default_gemfile_path(ws)) { |line| ... } click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 345
def self.run_bundler(ws, *commandline,
    bundler_version: ws.config.bundler_version,
    gem_home: ws.config.gems_gem_home,
    gemfile: default_gemfile_path(ws))
    bundle = Autobuild.programs["bundle"] || default_bundler(ws)

    Autoproj.bundler_with_unbundled_env do
        bundler_version_env =
            if bundler_version
                { "BUNDLER_VERSION" => bundler_version }
            else
                {}
            end
        target_env = Hash[
            "GEM_HOME" => gem_home,
            "GEM_PATH" => nil,
            "BUNDLE_GEMFILE" => gemfile,
            "RUBYOPT" => nil,
            "RUBYLIB" => rubylib_for_bundler,
        ].merge(bundler_version_env)
        ws.run("autoproj", "osdeps",
               bundle, *commandline,
               working_directory: File.dirname(gemfile),
               env: target_env) { |line| yield(line) if block_given? }
    end
end
run_bundler_install( ws, gemfile, *options, update: true, binstubs: nil, bundler_version: ws.config.bundler_version, gem_home: ws.config.gems_gem_home, gem_path: ws.config.gems_install_path ) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 297
def self.run_bundler_install(
    ws, gemfile, *options,
    update: true, binstubs: nil,
    bundler_version: ws.config.bundler_version,
    gem_home: ws.config.gems_gem_home,
    gem_path: ws.config.gems_install_path
)
    FileUtils.rm "#{gemfile}.lock" if update && File.file?("#{gemfile}.lock")

    options << "--path" << gem_path
    options << "--shebang" << Gem.ruby
    options << "--binstubs" << binstubs if binstubs

    apply_build_config(ws)

    connections = Set.new
    run_bundler(ws, "install", *options,
                bundler_version: bundler_version,
                gem_home: gem_home, gemfile: gemfile) do |line|
        case line
        when /Installing (.*)/
            Autobuild.message "  bundler: installing #{$1}"
        when /Fetching.*from (.*)/
            host = $1.gsub(/\.+$/, "")
            unless connections.include?(host)
                Autobuild.message "  bundler: connected to #{host}"
                connections << host
            end
        end
    end
end
with_prerelease(*value) { || ... } click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 13
def self.with_prerelease(*value)
    if value.empty?
        @with_prerelease
    else
        begin
            saved_flag = @with_prerelease
            @with_prerelease = value.first
            yield
        ensure
            @with_prerelease = saved_flag
        end
    end
end

Public Instance Methods

backup_clean(mapping) click to toggle source

@api private

Remove backups created by {#backup_files}

@param (see backup_file)

# File lib/autoproj/package_managers/bundler_manager.rb, line 291
def backup_clean(mapping)
    mapping.each do |_file, backup_file|
        FileUtils.rm backup_file if File.file?(backup_file)
    end
end
backup_files(mapping) click to toggle source

@api private

Create backup files matching a certain file mapping

@param [Hash<String,String>] mapping a mapping from the original

file to the file into which it should be backed up. The source
file might not exist.
# File lib/autoproj/package_managers/bundler_manager.rb, line 269
def backup_files(mapping)
    mapping.each do |file, backup_file|
        FileUtils.cp file, backup_file if File.file?(file)
    end
end
backup_restore(mapping) click to toggle source

@api private

Restore backups saved by {#backup_file}

@param (see backup_file)

# File lib/autoproj/package_managers/bundler_manager.rb, line 280
def backup_restore(mapping)
    mapping.each do |file, backup_file|
        FileUtils.cp backup_file, file if File.file?(backup_file)
    end
end
call_while_empty?() click to toggle source

(see Manager#call_while_empty?)

# File lib/autoproj/package_managers/bundler_manager.rb, line 36
def call_while_empty?
    !workspace_configuration_gemfiles.empty?
end
create_cache_copy(cache_dir, bundler_cache_dir) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 140
def create_cache_copy(cache_dir, bundler_cache_dir)
    valid = !File.exist?(bundler_cache_dir) ||
            File.directory?(bundler_cache_dir) ||
            File.symlink?(bundler_cache_dir)

    unless valid
        Autoproj.warn "cannot use #{cache_dir} as gem cache as "\
                      "#{bundler_cache_dir} already exists"
        return
    end

    # Gracefully upgrade from the symlinks
    FileUtils.rm_f bundler_cache_dir if File.symlink?(bundler_cache_dir)
    FileUtils.mkdir_p bundler_cache_dir

    Dir.glob(File.join(cache_dir, "*.gem")) do |path_src|
        path_dest = File.join(bundler_cache_dir, File.basename(path_src))
        next if File.exist?(path_dest)

        FileUtils.cp path_src, path_dest
    end
end
discover_bundle_rubylib(silent_errors: false) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 624
def discover_bundle_rubylib(silent_errors: false)
    require "bundler"
    gemfile = File.join(ws.prefix_dir, "gems", "Gemfile")
    silent_redirect = Hash.new
    silent_redirect[:err] = :close if silent_errors
    env = ws.env.resolved_env
    Tempfile.open "autoproj-rubylib" do |io|
        result = Autoproj.bundler_unbundled_system(
            Hash["GEM_HOME" => env["GEM_HOME"], "GEM_PATH" => env["GEM_PATH"],
                 "BUNDLE_GEMFILE" => gemfile, "RUBYOPT" => nil,
                 "RUBYLIB" => self.class.rubylib_for_bundler],
            Autobuild.tool("ruby"), "-rbundler/setup",
            "-e", "puts $LOAD_PATH",
            out: io, **silent_redirect
        )

        if result
            io.rewind
            io.readlines.map(&:chomp).find_all { |l| !l.empty? }
        end
    end
end
discover_rubylib() click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 603
def discover_rubylib
    require "bundler"
    Tempfile.open "autoproj-rubylib" do |io|
        result = Autoproj.bundler_unbundled_system(
            Hash["RUBYLIB" => nil],
            Autobuild.tool("ruby"), "-e", "puts $LOAD_PATH",
            out: io,
            err: "/dev/null"
        )
        if result
            io.rewind
            io.readlines.map(&:chomp).find_all { |l| !l.empty? }
        end
    end
end
initialize_environment() click to toggle source

Set up the workspace environment to work with the bundler-managed gems

# File lib/autoproj/package_managers/bundler_manager.rb, line 47
def initialize_environment
    env = ws.env

    config = ws.config

    env.add_path "PATH", File.join(ws.prefix_dir, "gems", "bin")
    env.add_path "PATH", File.join(ws.dot_autoproj_dir, "bin")
    env.set "GEM_HOME", config.gems_gem_home
    env.clear "GEM_PATH"
    if (bundler_version = config.bundler_version)
        env.set "BUNDLER_VERSION", bundler_version
    else
        env.clear "BUNDLER_VERSION"
    end

    gemfile_path = File.join(ws.prefix_dir, "gems", "Gemfile")
    env.set("BUNDLE_GEMFILE", gemfile_path) if File.file?(gemfile_path)

    if cache_dir && File.exist?(cache_dir)
        vendor_dir = File.join(File.dirname(gemfile_path), "vendor")
        FileUtils.mkdir_p vendor_dir
        bundler_cache_dir = File.join(vendor_dir, "cache")
        if File.writable?(cache_dir)
            create_cache_symlink(cache_dir, bundler_cache_dir)
        else
            Autoproj.warn "BundlerManager: #{cache_dir} is read-only "\
                          "copying the cache instead of symlinking it"
            create_cache_copy(cache_dir, bundler_cache_dir)
        end
    end

    Autobuild.programs["bundler"] =
        Autobuild.programs["bundle"] =
            File.join(ws.dot_autoproj_dir, "bin", "bundle")

    env.init_from_env "RUBYLIB"
    env.inherit "RUBYLIB"
    # Sanitize the rubylib we get from the environment by removing
    # anything that comes from Gem or Bundler
    original_rubylib =
        (env["RUBYLIB"] || "").split(File::PATH_SEPARATOR).find_all do |p|
            !p.start_with?(Bundler.rubygems.gem_dir) &&
                Bundler.rubygems.gem_path
                       .none? { |gem_p| p.start_with?(gem_p) }
        end

    # And discover the system's rubylib
    if (system_rubylib = discover_rubylib)
        # Do not explicitely add the system rubylib to the
        # environment, the interpreter will do it for us.
        #
        # This allows to use a binstub generated for one of ruby
        # interpreter version on our workspace
        env.system_env["RUBYLIB"] = []
        env.original_env["RUBYLIB"] = (original_rubylib - system_rubylib)
                                      .join(File::PATH_SEPARATOR)
    end

    ws.config.each_reused_autoproj_installation do |p|
        reused_w = ws.new(p)
        env.add_path "PATH", File.join(reused_w.prefix_dir, "gems", "bin")
    end

    prefix_gems = File.join(ws.prefix_dir, "gems")
    FileUtils.mkdir_p prefix_gems
    gemfile = File.join(prefix_gems, "Gemfile")
    unless File.exist?(gemfile)
        Ops.atomic_write(gemfile) do |io|
            dot_autoproj_gemfile = File.join(ws.dot_autoproj_dir, "Gemfile")
            io.puts "eval_gemfile \"#{dot_autoproj_gemfile}\""
        end
    end

    if (bundle_rubylib = discover_bundle_rubylib(silent_errors: true))
        update_env_rubylib(bundle_rubylib, system_rubylib)
    end
end
install(gems, filter_uptodate_packages: false, install_only: false) click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 513
def install(gems, filter_uptodate_packages: false, install_only: false)
    gemfile_path = self.class.default_gemfile_path(ws)
    root_dir = File.dirname(gemfile_path)
    gemfile_lock_path = "#{gemfile_path}.lock"
    backups = Hash[
        gemfile_path => "#{gemfile_path}.orig",
        gemfile_lock_path => "#{gemfile_lock_path}.orig"
    ]

    # Back up the existing gemfile, we'll restore it if something is
    # wrong to avoid leaving bundler in an inconsistent state
    backup_files(backups)
    unless File.file?("#{gemfile_path}.orig")
        Ops.atomic_write("#{gemfile_path}.orig") do |io|
            dot_autoproj_gemfile = File.join(ws.dot_autoproj_dir, "Gemfile")
            io.puts "eval_gemfile \"#{dot_autoproj_gemfile}\""
        end
    end

    gemfiles = []

    unless gems.empty?
        osdeps_gemfile_io = prepare_osdeps_gemfile(gems)
        gemfiles << osdeps_gemfile_io.path
    end

    gemfiles += workspace_configuration_gemfiles
    # The autoproj gemfile needs to be last, we really don't
    # want to mess it up
    gemfiles << File.join(ws.dot_autoproj_dir, "Gemfile")

    ruby_version = RUBY_VERSION.gsub(/\.\d+$/, ".0")
    gemfile_contents =
        merge_gemfiles(*gemfiles, ruby_version: "~> #{ruby_version}")

    FileUtils.mkdir_p root_dir
    updated = (!File.exist?(gemfile_path) ||
               File.read(gemfile_path) != gemfile_contents)
    if updated
        Ops.atomic_write(gemfile_path) do |io|
            io.puts gemfile_contents
        end
    end

    options = []
    binstubs_path = File.join(root_dir, "bin")
    if updated || !install_only || !File.file?("#{gemfile_path}.lock")
        self.class.run_bundler_install(ws, gemfile_path, *options,
                                       binstubs: binstubs_path)
    end

    if (bundle_rubylib = discover_bundle_rubylib)
        update_env_rubylib(bundle_rubylib)
    else
        raise NotCleanState, "bundler executed successfully, "\
                             "but the result was not in a clean state"
    end
rescue Exception
    Autoproj.warn "saved the new Gemfile in #{gemfile_path}.FAILED "\
                  "and restored the last Gemfile version"
    FileUtils.cp gemfile_path, "#{gemfile_path}.FAILED"
    backup_restore(backups)
    raise
ensure
    if binstubs_path
        FileUtils.rm_f File.join(binstubs_path, "bundle")
        FileUtils.rm_f File.join(binstubs_path, "bundler")
    end
    backup_clean(backups)
end
merge_gemfiles(*path, ruby_version: nil, unlock: []) click to toggle source

Parse the contents of a gemfile into a set of

# File lib/autoproj/package_managers/bundler_manager.rb, line 373
def merge_gemfiles(*path, ruby_version: nil, unlock: [])
    gems_remotes = Set.new
    dependencies = Hash.new do |h, k|
        h[k] = Hash.new do |i, j|
            i[j] = Hash.new do |a, b|
                a[b] = Array.new
            end
        end
    end
    path.each do |gemfile|
        bundler_def =
            begin Bundler::Dsl.evaluate(gemfile, nil, [])
            rescue Exception => e
                cleaned_message = e
                                  .message
                                  .gsub(/There was an error parsing([^:]+)/,
                                        "Error in gem definitions")
                                  .gsub(/#  from.*/, "")
                raise ConfigError, cleaned_message
            end
        gems_remotes |= bundler_def.send(:sources).rubygems_remotes.to_set
        bundler_def.dependencies.each do |d|
            d.groups.each do |group_name|
                if d.platforms.empty?
                    dependencies[group_name][""][d.name] = d
                else
                    d.platforms.each do |platform_name|
                        dependencies[group_name][platform_name][d.name] = d
                    end
                end
            end
        end
    end

    contents = []
    gems_remotes.each do |g|
        g = g.to_s
        g = g[0..-2] if g.end_with?("/")
        contents << "source '#{g}'"
    end
    if ruby_version
        contents << "ruby \"#{ruby_version}\" if respond_to?(:ruby)"
    end
    valid_keys = %w[group groups git path glob name branch ref tag
                    require submodules platform platforms type
                    source install_if]
    dependencies.each do |group_name, by_platform|
        contents << "group :#{group_name} do"
        by_platform.each do |platform_name, deps|
            deps = deps.values.sort_by(&:name)
            unless platform_name.empty?
                contents << "  platform :#{platform_name} do"
                platform_indent = "  "
            end
            deps.each do |d|
                if d.source
                    options = d.source.options.dup
                    options.delete_if { |k, _| !valid_keys.include?(k) }
                    options = options.map { |k, v| "#{k}: \"#{v}\"" }
                end
                contents << ["  #{platform_indent}gem \"#{d.name}\",
                             \"#{d.requirement}\"", *options].join(", ")
            end
            contents << "  end" unless platform_name.empty?
        end
        contents << "end"
    end
    contents.join("\n")
end
prepare_osdeps_gemfile(gems) click to toggle source

Prepare a Gemfile that contains osdeps gem declarations

@param [Array<String,Hash>] gems osdeps declarations as understood

by {GemEntry.parse}

@return [File]

# File lib/autoproj/package_managers/bundler_manager.rb, line 589
def prepare_osdeps_gemfile(gems)
    io = Tempfile.open "autoproj-gemfile"
    io.puts "source \"https://rubygems.org\""
    gems.map { |entry| GemEntry.parse(entry) }
        .sort_by(&:name)
        .each { |entry| io.puts entry.to_gemfile_line }

    io.flush
    io
rescue Exception
    io&.close
    raise
end
strict?() click to toggle source

(see Manager#strict?)

# File lib/autoproj/package_managers/bundler_manager.rb, line 41
def strict?
    true
end
update_env_rubylib(bundle_rubylib, system_rubylib = discover_rubylib) click to toggle source

@api private

Update RUBYLIB to add the gems that are part of the bundler install

@param [Array<String>] bundle_rubylib the rubylib entries reported

by bundler

@param [Array<String>] system_rubylib the rubylib entries that are

set by the underlying ruby interpreter itself
# File lib/autoproj/package_managers/bundler_manager.rb, line 252
def update_env_rubylib(bundle_rubylib, system_rubylib = discover_rubylib)
    current = (ws.env.resolved_env["RUBYLIB"] || "")
              .split(File::PATH_SEPARATOR) + system_rubylib
    (bundle_rubylib - current).each do |p|
        ws.env.add_path("RUBYLIB", p)
    end
end
workspace_configuration_gemfiles() click to toggle source
# File lib/autoproj/package_managers/bundler_manager.rb, line 443
def workspace_configuration_gemfiles
    gemfiles = []
    ws.manifest.each_package_set do |source|
        pkg_set_gemfile = File.join(source.local_dir, "Gemfile")
        if source.local_dir && File.file?(pkg_set_gemfile)
            gemfiles << pkg_set_gemfile
        end
    end
    # In addition, look into overrides.d
    Dir.glob(File.join(ws.overrides_dir, "*.gemfile")) do |overrides_gemfile|
        gemfiles << overrides_gemfile
    end
    gemfiles
end