bin/autoproj_install in autoproj-2.0.0.rc4 vs bin/autoproj_install in autoproj-2.0.0.rc5

- old
+ new

@@ -3,10 +3,11 @@ if RUBY_VERSION < "1.9.2" STDERR.puts "autoproj requires Ruby >= 1.9.2" exit 1 end +require 'pathname' require 'optparse' require 'fileutils' require 'yaml' module Autoproj @@ -15,38 +16,95 @@ # clean root # # It can be required standalone (i.e. does not depend on anything else than # ruby and the ruby standard library) class Install + class UnexpectedBinstub < RuntimeError; end + # The directory in which to install autoproj attr_reader :root_dir # Content of the Gemfile generated to install autoproj itself attr_accessor :gemfile + # The environment that is passed to the bundler installs + attr_reader :env def initialize(root_dir) @root_dir = root_dir - @gemfile = default_gemfile_contents + if File.file?(autoproj_gemfile_path) + @gemfile = File.read(autoproj_gemfile_path) + else + @gemfile = default_gemfile_contents + end + @private_bundler = false @private_autoproj = false @private_gems = false + @local = false + @env = self.class.clean_env end + def self.clean_env + env = Hash.new + env['RUBYLIB'] = [] + env['GEM_PATH'] = [] + %w{PATH GEM_HOME}.each do |name| + env[name] = sanitize_env(ENV[name] || "") + end + env['BUNDLE_GEMFILE'] = nil + env + end + + def env_for_child + env.inject(Hash.new) do |h, (k, v)| + h[k] = (v.join(File::PATH_SEPARATOR) if v && !v.empty?) + h + end + end + + def self.sanitize_env(value) + value.split(File::PATH_SEPARATOR). + find_all { |p| !in_workspace?(p) } + end + + def self.in_workspace?(base_dir) + path = Pathname.new(base_dir) + while !path.root? + if (path + ".autoproj").exist? || (path + "autoproj").exist? + return true + end + path = path.parent + end + return false + end + + def dot_autoproj; File.join(root_dir, '.autoproj') end def bin_dir; File.join(dot_autoproj, 'bin') end def bundler_install_dir; File.join(dot_autoproj, 'bundler') end def autoproj_install_dir; File.join(dot_autoproj, 'autoproj') end # The path to the gemfile used to install autoproj def autoproj_gemfile_path; File.join(autoproj_install_dir, 'Gemfile') end def autoproj_config_path; File.join(dot_autoproj, 'config.yml') end + # Whether we can access the network while installing + def local?; !!@local end + # (see #local?) + def local=(flag); @local = flag end + # Whether bundler should be installed locally in {#dot_autoproj} def private_bundler?; @private_bundler end + # (see #private_bundler?) + def private_bundler=(flag); @private_bundler = flag end # Whether autoproj should be installed locally in {#dot_autoproj} def private_autoproj?; @private_autoproj end + # (see #private_autoproj?) + def private_autoproj=(flag); @private_autoproj = flag end # Whether bundler should be installed locally in the workspace # prefix directory def private_gems?; @private_gems end + # (see #private_gems?) + def private_gems=(flag); @private_gems = flag end def guess_gem_program ruby_bin = RbConfig::CONFIG['RUBY_INSTALL_NAME'] ruby_bindir = RbConfig::CONFIG['bindir'] @@ -66,18 +124,22 @@ # The content of the default {#gemfile} # # @param [String] autoproj_version a constraint on the autoproj version # that should be used # @return [String] - def default_gemfile_contents(autoproj_version = ">= 0") + def default_gemfile_contents(autoproj_version = ">= 2.0.0.a") ["source \"https://rubygems.org\"", - "gem \"autoproj\", \"#{autoproj_version}\""].join("\n") + "gem \"autoproj\", \"#{autoproj_version}\"", + "gem \"utilrb\", \">= 3.0.0.a\""].join("\n") end # Parse the provided command line options and returns the non-options def parse_options(args = ARGV) options = OptionParser.new do |opt| + opt.on '--local', 'do not access the network (may fail)' do + @local = true + end opt.on '--private-bundler', 'install bundler locally in the workspace' do @private_bundler = true end opt.on '--private-autoproj', 'install autoproj locally in the workspace' do @private_autoproj = true @@ -102,32 +164,41 @@ def install_bundler gem_program = guess_gem_program puts "Detected 'gem' to be #{gem_program}" + local = ['--local'] if local? + result = system( - Hash['GEM_PATH' => nil, - 'GEM_HOME' => bundler_install_dir], + env_for_child.merge('GEM_PATH' => nil, 'GEM_HOME' => bundler_install_dir), gem_program, 'install', '--no-document', '--no-user-install', '--no-format-executable', + *local, "--bindir=#{File.join(bundler_install_dir, 'bin')}", 'bundler') if !result STDERR.puts "FATAL: failed to install bundler in #{dot_autoproj}" exit 1 end + env['GEM_PATH'] << bundler_install_dir + env['PATH'] << File.join(bundler_install_dir, 'bin') File.join(bin_dir, 'bundler') end def save_env_sh env = Autobuild::Environment.new - path = [] - if private_bundler? - env.push_path 'PATH', File.join(bundler_install_dir, 'bin') - env.push_path 'GEM_PATH', bundler_install_dir + env.prepare + + %w{GEM_HOME GEM_PATH}.each do |name| + value = self.env[name] + if value.empty? + env.unset name + else + env.set name, *value + end end env.push_path 'PATH', File.join(autoproj_install_dir, 'bin') - env.inherit 'PATH' + if private_autoproj? env.push_path 'GEM_PATH', autoproj_install_dir end # Generate environment files right now, we can at least use bundler @@ -147,37 +218,72 @@ FileUtils.mkdir_p File.dirname(autoproj_gemfile_path) File.open(autoproj_gemfile_path, 'w') do |io| io.write gemfile end end - + + ENV_BUNDLE_GEMFILE_RX = /^(\s*ENV\[['"]BUNDLE_GEMFILE['"]\]\s*)(?:\|\|)?=/ + def install_autoproj(bundler) # Force bundler to update. If the user does not want this, let him specify a # Gemfile with tighter version constraints lockfile = File.join(File.dirname(autoproj_gemfile_path), 'Gemfile.lock') if File.exist?(lockfile) FileUtils.rm lockfile end - env = Hash['BUNDLE_GEMFILE' => nil, 'RUBYLIB' => nil] opts = Array.new + opts << '--local' if local? + env = env_for_child if private_autoproj? - env = Hash['GEM_PATH' => bundler_install_dir, - 'GEM_HOME' => nil] + env = env.merge( + 'GEM_PATH' => bundler_install_dir, + 'GEM_HOME' => nil) opts << "--clean" << "--path=#{autoproj_install_dir}" end + binstubs_path = File.join(autoproj_install_dir, 'bin') + result = system(env, - bundler, 'install', + Gem.ruby, bundler, 'install', "--gemfile=#{autoproj_gemfile_path}", - "--binstubs=#{File.join(autoproj_install_dir, 'bin')}", + "--binstubs=#{binstubs_path}", *opts) if !result STDERR.puts "FATAL: failed to install autoproj in #{dot_autoproj}" exit 1 end + + # Now tune the binstubs to force the usage of the autoproj + # gemfile. Otherwise, they get BUNDLE_GEMFILE from the + # environment by default + Dir.glob(File.join(binstubs_path, '*')) do |path| + next if !File.file?(path) + # Do NOT do that for bundler, otherwise it will fail with an + # "already loaded gemfile" message once we e.g. try to do + # 'bundler install --gemfile=NEW_GEMFILE' + next if File.basename(path) == 'bundler' + + lines = File.readlines(path) + matched = false + filtered = lines.map do |l| + matched ||= (ENV_BUNDLE_GEMFILE_RX === l) + l.gsub(ENV_BUNDLE_GEMFILE_RX, '\\1=') + end + if !matched + raise UnexpectedBinstub, "expected #{path} to contain a line looking like ENV['BUNDLE_GEMFILE'] ||= but could not find one" + end + File.open(path, 'w') do |io| + io.write filtered.join("") + end + end + + env['PATH'] << File.join(autoproj_install_dir, 'bin') + if private_autoproj? + env['GEM_PATH'] << autoproj_install_dir + end end def update_configuration if File.exist?(autoproj_config_path) config = YAML.load(File.read(autoproj_config_path)) || Hash.new @@ -190,28 +296,95 @@ File.open(autoproj_config_path, 'w') do |io| YAML.dump(config, io) end end + def find_in_clean_path(command) + clean_path = env_for_child['PATH'].split(File::PATH_SEPARATOR) + clean_path.each do |p| + full_path = File.join(p, command) + if File.file?(full_path) + return full_path + end + end + nil + end + + def find_bundler + clean_env = env_for_child + Gem.paths = Hash[ + 'GEM_HOME' => clean_env['GEM_HOME'] || Gem.default_dir, + 'GEM_PATH' => clean_env['GEM_PATH'] || nil + ] + # Here, we're getting into the esotheric + # + # The problem is that e.g. Ubuntu and Debian install an + # operating_system.rb file that sets proper OS defaults. Some + # autoproj installs have it in their RUBYLIB but should not + # because of limitations of autoproj 1.x. This leads to + # Gem.bindir being *not* valid for subprocesses + # + # So, we're calling 'gem' as a subcommand to discovery the + # actual bindir + bindir = IO.popen(clean_env, [Gem.ruby, '-e', 'puts Gem.bindir']).read + if bindir + env['PATH'].unshift bindir.chomp + else + STDERR.puts "FATAL: cannot run #{Gem.ruby} -e 'puts Gem.bindir'" + exit 1 + end + + bundler = find_in_clean_path('bundler') + if !bundler + clean_path = env_for_child['PATH'] + STDERR.puts "FATAL: cannot find 'bundler' in PATH=#{clean_path}" + if ENV['PATH'] != clean_path + STDERR.puts " it appears that you already have some autoproj-generated env.sh loaded" + STDERR.puts " - if you are running 'autoproj upgrade', please contact the autoproj author at https://github.com/rock-core/autoproj/issues/new" + STDERR.puts " - if you are running an install, try again in a console where the env.sh is not loaded" + exit 1 + else + STDERR.puts " the recommended action is to install it manually first by running 'gem install bundler'" + STDERR.puts " or call this command again with --private-bundler to have it installed in the workspace" + exit 1 + end + end + bundler + end + def install if private_bundler? puts "Installing bundler in #{bundler_install_dir}" bundler = install_bundler + else + bundler = find_bundler + puts "Detected bundler at #{bundler}" end save_gemfile puts "Installing autoproj in #{dot_autoproj}" - install_autoproj(bundler || 'bundler') + install_autoproj(bundler) end # Actually perform the install - def run - install - ENV['BUNDLE_GEMFILE'] = autoproj_gemfile_path - require 'bundler' - Bundler.setup - require 'autobuild' - save_env_sh - update_configuration + def run(stage2: false) + if stage2 + require 'autobuild' + save_env_sh + else + install + + env_for_child.each do |k, v| + if v + ENV[k] = v + else + ENV.delete(k) + end + end + ENV['BUNDLE_GEMFILE'] = autoproj_gemfile_path + update_configuration + exec Gem.ruby, File.join(autoproj_install_dir, 'bin', 'autoproj'), + 'install-stage2', root_dir + end end end end end