#! /usr/bin/ruby if RUBY_VERSION < "1.8.7" STDERR.puts "autoproj requires Ruby >= 1.8.7" exit 1 end require 'set' curdir_entries = Dir.entries('.').to_set - [".", "..", "autoproj_bootstrap", ".gems", 'env.sh'].to_set if !curdir_entries.empty? && ENV['AUTOPROJ_BOOTSTRAP_IGNORE_NONEMPTY_DIR'] != '1' while true print "The current directory is not empty, continue bootstrapping anyway ? [yes] " STDOUT.flush answer = STDIN.readline.chomp if answer == "no" exit elsif answer == "" || answer == "yes" # Set the environment variable since we might restart the # autoproj_bootstrap script and -- anyway -- will run "autoproj # bootstrap" later on break else STDOUT.puts "invalid answer. Please answer 'yes' or 'no'" STDOUT.flush end end end # Environment is clean, so just mark it as so unconditionally ENV['AUTOPROJ_BOOTSTRAP_IGNORE_NONEMPTY_DIR'] = '1' needed_gem_home = ENV['AUTOPROJ_GEM_HOME'] || "#{Dir.pwd}/.gems" if $LOADED_FEATURES.find { |str| str =~ /bygems/ } if ENV['GEM_HOME'] != needed_gem_home require 'rbconfig' RUBY = RbConfig::CONFIG['RUBY_INSTALL_NAME'] ENV['GEM_HOME'] = needed_gem_home exec RUBY, __FILE__, *ARGV end end ENV['GEM_HOME'] = needed_gem_home ENV['PATH'] = "#{ENV['GEM_HOME']}/bin:#{ENV['PATH']}" require 'yaml' require 'set' module Autoproj class ConfigError < RuntimeError; end class << self attr_reader :verbose end def self.color(string, *args) string end def self.progress(str) STDERR.puts " #{str}" end end module Autobuild def self.do_update true end def self.progress(str) STDERR.puts " #{str}" end class << self attr_reader :programs end @programs = Hash.new def self.tool(name) # Let the ability to set programs[name] to nil to make sure we don't use # that program. This is used later on in this file to make sure we # aren't using the wrong rubygems binary if programs.has_key?(name) programs[name] else name end end module Subprocess def self.run(name, phase, *cmd) output = `#{cmd.join(" ")}` if $?.exitstatus != 0 STDERR.puts "ERROR: failed to run #{cmd.join(" ")}" STDERR.puts "ERROR: command output is: #{output}" exit 1 end end end end require 'tempfile' module Autoproj class OSDependencies class << self # When requested to load a file called X.Y, the osdeps code will # also look for files called X-suffix.Y, where 'suffix' is an # element in +osdeps_suffixes+ # # A usage of this functionality is to make loading conditional to # the available version of certain tools, namely Ruby. Autoproj for # instance adds ruby18 when started on Ruby 1.8 and ruby19 when # started on Ruby 1.9 attr_reader :suffixes end @suffixes = [] def self.load(file) if !File.file?(file) raise ArgumentError, "no such file or directory #{file}" end candidates = [file] candidates.concat(suffixes.map { |s| "#{file}-#{s}" }) result = OSDependencies.new candidates.each do |file| next if !File.file?(file) file = File.expand_path(file) begin data = YAML.load(File.read(file)) || Hash.new verify_definitions(data) rescue ArgumentError => e raise ConfigError.new, "error in #{file}: #{e.message}", e.backtrace end result.merge(OSDependencies.new(data, file)) end result end class << self attr_reader :aliases attr_accessor :force_osdeps attr_accessor :gem_with_prerelease attr_accessor :gem_doc end @aliases = Hash.new @gem_doc = false attr_writer :silent def silent?; @silent end def self.alias(old_name, new_name) @aliases[new_name] = old_name end def self.ruby_version_keyword if RUBY_VERSION < "1.9.0" then "ruby18" else "ruby19" end end def self.autodetect_ruby self.alias(ruby_version_keyword, "ruby") end self.suffixes << ruby_version_keyword AUTOPROJ_OSDEPS = File.join(File.expand_path(File.dirname(__FILE__)), 'default.osdeps') def self.load_default file = ENV['AUTOPROJ_DEFAULT_OSDEPS'] || AUTOPROJ_OSDEPS if !File.file?(file) Autoproj.progress "WARN: #{file} (from AUTOPROJ_DEFAULT_OSDEPS) is not a file, falling back to #{AUTOPROJ_OSDEPS}" file = AUTOPROJ_OSDEPS end OSDependencies.load(file) end # The information contained in the OSdeps files, as a hash attr_reader :definitions # All the information contained in all the OSdeps files, as a mapping # from the OSdeps package name to [osdeps_file, definition] pairs attr_reader :all_definitions # The information as to from which osdeps file the current package # information in +definitions+ originates. It is a mapping from the # package name to the osdeps file' full path attr_reader :sources # The Gem::SpecFetcher object that should be used to query RubyGems, and # install RubyGems packages def gem_fetcher if !@gem_fetcher Autobuild.progress "looking for RubyGems updates" @gem_fetcher = Gem::SpecFetcher.fetcher end @gem_fetcher end def initialize(defs = Hash.new, file = nil) @definitions = defs.to_hash @all_definitions = Hash.new { |h, k| h[k] = Array.new } @sources = Hash.new @installed_packages = Array.new if file defs.each_key do |package_name| sources[package_name] = file all_definitions[package_name] << [[file], defs[package_name]] end end @silent = true @filter_uptodate_packages = true end # Returns the full path to the osdeps file from which the package # definition for +package_name+ has been taken def source_of(package_name) sources[package_name] end # Merges the osdeps information of +info+ into +self+. If packages are # defined in both OSDependencies objects, the information in +info+ # takes precedence def merge(info) root_dir = nil @definitions = definitions.merge(info.definitions) do |h, v1, v2| if v1 != v2 root_dir ||= "#{Autoproj.root_dir}/" old = source_of(h).gsub(root_dir, '') new = info.source_of(h).gsub(root_dir, '') Autoproj.warn("osdeps definition for #{h}, previously defined in #{old} overridden by #{new}") end v2 end @sources = sources.merge(info.sources) @all_definitions = all_definitions.merge(info.all_definitions) do |package_name, all_defs, new_all_defs| all_defs = all_defs.dup new_all_defs = new_all_defs.dup new_all_defs.delete_if do |files, data| if entry = all_defs.find { |_, d| d == data } entry[0] |= files end end all_defs.concat(new_all_defs) end end # Perform some sanity checks on the given osdeps definitions def self.verify_definitions(hash) hash.each do |key, value| if !key.kind_of?(String) raise ArgumentError, "invalid osdeps definition: found an #{key.class}. Don't forget to put quotes around numbers" end next if !value if value.kind_of?(Array) || value.kind_of?(Hash) verify_definitions(value) else if !value.kind_of?(String) raise ArgumentError, "invalid osdeps definition: found an #{value.class}. Don't forget to put quotes around numbers" end end end end # Returns true if it is possible to install packages for the operating # system on which we are installed def self.supported_operating_system? if @supported_operating_system.nil? os_names, _ = operating_system @supported_operating_system = if !os_names then false else os_names.any? { |os_name| OS_AUTO_PACKAGE_INSTALL.has_key?(os_name) } end end return @supported_operating_system end # Autodetects the operating system name and version # # +osname+ is the operating system name, all in lowercase (e.g. ubuntu, # arch, gentoo, debian) # # +versions+ is a set of names that describe the OS version. It includes # both the version number (as a string) and/or the codename if there is # one. # # Examples: ['debian', ['sid', 'unstable']] or ['ubuntu', ['lucid lynx', '10.04']] def self.operating_system(options = Hash.new) # Validate the options. We check on the availability of # validate_options as to not break autoproj_bootstrap (in which # validate_options is not available) options = if Kernel.respond_to?(:validate_options) Kernel.validate_options options, :force => false else options.dup end if !options[:force] if !@operating_system.nil? return @operating_system elsif Autoproj.has_config_key?('operating_system') os = Autoproj.user_config('operating_system') if os.respond_to?(:to_ary) if os[0].respond_to?(:to_ary) && os[0].all? { |s| s.respond_to?(:to_str) } && os[1].respond_to?(:to_ary) && os[1].all? { |s| s.respond_to?(:to_str) } @operating_system = os return os end end end end if user_os = ENV['AUTOPROJ_OS'] if user_os.empty? @operating_system = false else names, versions = user_os.split(':') @operating_system = [names.split(','), versions.split(',')] end else Autoproj.progress " autodetecting the operating system" name, versions = os_from_lsb if name if name != "debian" if File.exists?("/etc/debian_version") @operating_system = [[name, "debian"], versions] else @operating_system = [[name], versions] end end end end if @operating_system.nil? # Need to do some heuristics unfortunately @operating_system = if File.exists?('/etc/debian_version') codenames = [File.read('/etc/debian_version').strip] if codenames.first =~ /sid/ versions = codenames + ["unstable", "sid"] end [['debian'], versions] elsif File.exists?('/etc/gentoo-release') release_string = File.read('/etc/gentoo-release').strip release_string =~ /^.*([^\s]+)$/ version = $1 [['gentoo'], [version]] elsif File.exists?('/etc/arch-release') [['arch'], []] end end if !@operating_system return end # Normalize the names to lowercase names, versions = @operating_system[0], @operating_system[1] names = names.map(&:downcase) versions = versions.map(&:downcase) if !versions.include?('default') versions += ['default'] end @operating_system = [names, versions] Autoproj.change_option('operating_system', @operating_system, true) @operating_system end def self.os_from_lsb has_lsb_release = `which lsb_release` return unless $?.success? distributor = `lsb_release -i -s` distributor = distributor.strip.downcase codename = `lsb_release -c -s`.strip.downcase version = `lsb_release -r -s`.strip.downcase return [distributor, [codename, version]] end # On a dpkg-enabled system, checks if the provided package is installed # and returns true if it is the case def self.dpkg_package_installed?(package_name) if !@dpkg_installed_packages @dpkg_installed_packages = Set.new dpkg_status = File.readlines('/var/lib/dpkg/status') current_packages = [] is_installed = false dpkg_status.each do |line| line = line.chomp if line == "" if is_installed current_packages.each do |pkg| @dpkg_installed_packages << pkg end current_packages.clear is_installed = false end elsif line =~ /Package: (.*)$/ current_packages << $1 elsif line =~ /Provides: (.*)$/ current_packages.concat($1.split(',').map(&:strip)) elsif line == "Status: install ok installed" is_installed = true end end end if package_name =~ /^(\w[a-z0-9+-.]+)/ @dpkg_installed_packages.include?($1) else Autoproj.progress "WARN: #{package_name} is not a valid Debian package name" false end end GAIN_ROOT_ACCESS = <<-EOSCRIPT # Gain root access using sudo if test `id -u` != "0"; then exec sudo /bin/bash $0 "$@" fi EOSCRIPT OS_PACKAGE_CHECK = { 'debian' => method(:dpkg_package_installed?), 'ubuntu' => method(:dpkg_package_installed?) } OS_USER_PACKAGE_INSTALL = { 'debian' => "apt-get install '%s'", 'ubuntu' => "apt-get install '%s'", 'gentoo' => "emerge '%s'", 'arch' => "pacman '%s'" } OS_AUTO_PACKAGE_INSTALL = { 'debian' => "export DEBIAN_FRONTEND=noninteractive; apt-get install -y '%s'", 'ubuntu' => "export DEBIAN_FRONTEND=noninteractive; apt-get install -y '%s'", 'gentoo' => "emerge --noreplace '%s'", 'arch' => "pacman -Sy --noconfirm '%s'" } NO_PACKAGE = 0 WRONG_OS = 1 WRONG_OS_VERSION = 2 IGNORE = 3 PACKAGES = 4 UNKNOWN_OS = 7 AVAILABLE = 10 # Check for the definition of +name+ for this operating system # # It can return # # NO_PACKAGE:: # there are no package definition for +name # UNKNOWN_OS:: # this is not an OS autoproj knows how to deal with # WRONG_OS:: # there are a package definition, but not for this OS # WRONG_OS_VERSION:: # there is a package definition for this OS, but not for this # particular version of the OS # IGNORE:: # there is a package definition that told us to ignore the package # [PACKAGES, definition]:: # +definition+ is an array of package names that this OS's package # manager can understand def resolve_package(name) os_names, os_versions = OSDependencies.operating_system dep_def = definitions[name] if !dep_def return NO_PACKAGE elsif dep_def == 'ignore' return IGNORE elsif dep_def == 'nonexistent' return WRONG_OS_VERSION elsif !os_names return UNKNOWN_OS end # Find a matching entry for the OS name os_entry = nil os_names.find do |os_name| os_entry = dep_def.find do |name_list, _| name_list.split(','). map(&:downcase). any? { |n| n == os_name } end end if !os_entry return WRONG_OS end data = os_entry.last # This package does not need to be installed on this operating system (example: build tools on Gentoo) if !data || data == "ignore" return IGNORE end # If data is a hash, it means we have to check the OS version as well if data.kind_of?(Hash) # Look in +data+ for specific version. We look, in order, in the # os_versions field (detected OS versions), and return the first # matching entry version_entry = nil # First, look for an exact match found = os_versions.find do |os_ver| version_entry = data.find do |version_list, _| version_list.to_s.split(','). map(&:downcase). any? { |v| v == os_ver } end end if !found # Then relax the matching ... found = os_versions.find do |os_ver| version_entry = data.find do |version_list, _| version_list.to_s.split(','). map(&:downcase). any? { |v| Regexp.new(v) =~ os_ver } end end end if Autoproj.verbose Autoproj.progress "selected OS version #{found} for osdep #{name}: #{version_entry.inspect}" end if !version_entry return WRONG_OS_VERSION end data = version_entry.last end if data.respond_to?(:to_str) && data.to_str == 'nonexistent' return WRONG_OS_VERSION end if data.respond_to?(:to_ary) # List of packages return [PACKAGES, data] elsif data.respond_to?(:to_str) # Single package return [PACKAGES, [data.to_str]] else raise ConfigError.new, "invalid package specification #{data} in #{source_of(name)}" end end # Resolves the given OS dependencies into the actual packages that need # to be installed on this particular OS. # # Raises ConfigError if some packages can't be found def resolve_os_dependencies(dependencies) os_names, _ = OSDependencies.operating_system os_packages = [] dependencies.each do |name| result = resolve_package(name) if result == NO_PACKAGE raise ConfigError.new, "there is no osdeps definition for #{name}" elsif result == WRONG_OS raise ConfigError.new, "there is an osdeps definition for #{name}, but not for this operating system" elsif result == WRONG_OS_VERSION raise ConfigError.new, "there is an osdeps definition for #{name}, but not for this particular operating system version" elsif result == IGNORE next elsif result[0] == PACKAGES os_packages.concat(result[1]) end end if !os_names.any? { |os_name| OS_AUTO_PACKAGE_INSTALL.has_key?(os_name) } raise ConfigError.new, "I don't know how to install packages on #{os_names.first}" end return os_packages end def generate_user_os_script(os_names, os_packages) user_package_install = nil os_names.find do |os_name| user_package_install = OS_USER_PACKAGE_INSTALL[os_name] end if user_package_install (user_package_install % [os_packages.join("' '")]) else generate_auto_os_script(os_names, os_packages) end end def generate_auto_os_script(os_names, os_packages) auto_package_install = nil os_names.find do |os_name| auto_package_install = OS_AUTO_PACKAGE_INSTALL[os_name] end (auto_package_install % [os_packages.join("' '")]) end # Returns true if +name+ is an acceptable OS package for this OS and # version def has?(name) availability_of(name) == AVAILABLE end # If +name+ is an osdeps that is available for this operating system, # returns AVAILABLE. Otherwise, returns the same error code than # resolve_package. def availability_of(name) osdeps, gemdeps = partition_packages([name].to_set) if !osdeps.empty? osdeps.each do |dep_name| status = resolve_package(dep_name) if !status.respond_to?(:to_ary) && status != IGNORE return status end end AVAILABLE elsif !gemdeps.empty? AVAILABLE else NO_PACKAGE end end # call-seq: # partition_packages(package_names) => os_packages, gem_packages # # Resolves the package names listed in +package_set+, and returns a set # of packages that have to be installed using the platform's native # package manager, and the set of packages that have to be installed # using Ruby's package manager, RubyGems. # # The os_packages arrays is a list of names (strings) # # The gem_packages arrays is a list of [name, version] pairs. +version+ # is a version string, that is set only if the package has been given # with a =VERSION specification, e.g. # # gems: # hoe=1.8.0 # # Raises ConfigError if an error exists in the osdeps files, and returns # empty sets if the package can't be found def partition_packages(package_set) package_set = package_set. map { |name| OSDependencies.aliases[name] || name }. to_set osdeps, gems = [], [] package_set.to_set.each do |name| pkg_def = definitions[name] if !pkg_def # Error cases are taken care of later, because that is were # the automatic/manual osdeps logic lies osdeps << name next end pkg_def = pkg_def.dup if pkg_def.respond_to?(:to_str) case(pkg_def.to_str) when "ignore" then osdeps << name when "gem" then gems << name else # This is *not* handled later, as is the absence of a # package definition. The reason is that it is a bad # configuration file, and should be fixed by the user raise ConfigError.new, "unknown OS-independent package management type #{pkg_def} for #{name}" end else pkg_def.delete_if do |distrib_name, defs| if distrib_name == "gem" gems.concat([*defs]) true end end if !pkg_def.empty? osdeps << name end end end gems.map! do |name| if name =~ /^([^><=~]*)([><=~]+.*)$/ [$1.strip, $2.strip] else [name] end end return osdeps, gems end def guess_gem_program if Autobuild.programs['gem'] return Autobuild.programs['gem'] end ruby_bin = Config::CONFIG['RUBY_INSTALL_NAME'] if ruby_bin =~ /^ruby(.+)$/ Autobuild.programs['gem'] = "gem#{$1}" else Autobuild.programs['gem'] = "gem" end end # Returns true if the osdeps system knows how to remove uptodate # packages from the needs-to-be-installed package list on this OS def can_filter_uptodate_packages? os_names, _ = OSDependencies.operating_system !!os_names.any? { |os_name| OS_PACKAGE_CHECK[os_name] } end # Returns the set of packages in +packages+ that are not already # installed on this OS, if it is supported def filter_uptodate_os_packages(packages, os_names) check_method = nil os_names.find do |os_name| check_method = OS_PACKAGE_CHECK[os_name] end return packages.dup if !check_method packages.find_all { |pkg| !check_method[pkg] } end # Returns the set of RubyGem packages in +packages+ that are not already # installed, or that can be upgraded def filter_uptodate_gems(gems) # Don't install gems that are already there ... gems = gems.dup gems.delete_if do |name, version| version_requirements = Gem::Requirement.new(version || '>= 0') installed = if Gem::Specification.respond_to?(:find_by_name) begin [Gem::Specification.find_by_name(name, version_requirements)] rescue Gem::LoadError [] end else Gem.source_index.find_name(name, version_requirements) end if !installed.empty? && Autobuild.do_update # Look if we can update the package ... dep = Gem::Dependency.new(name, version_requirements) available = gem_fetcher.find_matching(dep, true, true, OSDependencies.gem_with_prerelease) installed_version = installed.map(&:version).max available_version = available.map { |(name, v), source| v }.max if !available_version if version raise ConfigError.new, "cannot find any gem with the name '#{name}' and version #{version}" else raise ConfigError.new, "cannot find any gem with the name '#{name}'" end end needs_update = (available_version > installed_version) !needs_update else !installed.empty? end end gems end HANDLE_ALL = 'all' HANDLE_RUBY = 'ruby' HANDLE_OS = 'os' HANDLE_NONE = 'none' def self.osdeps_mode_option_unsupported_os long_doc =<<-EOT The software packages that autoproj will have to build may require other prepackaged softwares (a.k.a. OS dependencies) to be installed (RubyGems packages, packages from your operating system/distribution, ...). Autoproj is usually able to install those automatically, but unfortunately your operating system is not (yet) supported by autoproj's osdeps mechanism, it can only offer you some limited support. RubyGem packages are a cross-platform mechanism, and are therefore supported. However, you will have to install the kind of OS dependencies (so-called OS packages) This option is meant to allow you to control autoproj's behaviour while handling OS dependencies. * if you say "ruby", the RubyGem packages will be installed. * if you say "none", autoproj will not do anything related to the OS dependencies. As any configuration value, the mode can be changed anytime by calling an autoproj operation with the --reconfigure option (e.g. autoproj update --reconfigure). Finally, OS dependencies can be installed by calling "autoproj osdeps" with the corresponding option (--all, --ruby, --os or --none). Calling "autoproj osdeps" without arguments will also give you information as to what you should install to compile the software successfully. So, what do you want ? (ruby or none) EOT message = [ "Which prepackaged software (a.k.a. 'osdeps') should autoproj install automatically (ruby, none) ?", long_doc.strip ] Autoproj.configuration_option 'osdeps_mode', 'string', :default => 'ruby', :doc => message, :possible_values => %w{ruby none}, :lowercase => true end def self.osdeps_mode_option_supported_os long_doc =<<-EOT The software packages that autoproj will have to build may require other prepackaged softwares (a.k.a. OS dependencies) to be installed (RubyGems packages, packages from your operating system/distribution, ...). Autoproj is able to install those automatically for you. Advanced users may want to control this behaviour. Additionally, the installation of some packages require administration rights, which you may not have. This option is meant to allow you to control autoproj's behaviour while handling OS dependencies. * if you say "all", it will install all packages automatically. This requires root access thru 'sudo' * if you say "ruby", only the Ruby packages will be installed. Installing these packages does not require root access. * if you say "os", only the OS-provided packages will be installed. Installing these packages requires root access. * if you say "none", autoproj will not do anything related to the OS dependencies. As any configuration value, the mode can be changed anytime by calling an autoproj operation with the --reconfigure option (e.g. autoproj update --reconfigure). Finally, OS dependencies can be installed by calling "autoproj osdeps" with the corresponding option (--all, --ruby, --os or --none). So, what do you want ? (all, ruby, os or none) EOT message = [ "Which prepackaged software (a.k.a. 'osdeps') should autoproj install automatically (all, ruby, os, none) ?", long_doc.strip ] Autoproj.configuration_option 'osdeps_mode', 'string', :default => 'all', :doc => message, :possible_values => %w{all ruby os none}, :lowercase => true end def self.define_osdeps_mode_option if supported_operating_system? osdeps_mode_option_supported_os else osdeps_mode_option_unsupported_os end end def self.osdeps_mode_string_to_value(string) string = string.downcase case string when 'all' then HANDLE_ALL when 'ruby' then HANDLE_RUBY when 'os' then HANDLE_OS when 'none' then HANDLE_NONE else raise ArgumentError, "invalid osdeps mode string '#{string}'" end end # If set to true (the default), #install will try to remove the list of # already uptodate packages from the installed packages. Set to false to # install all packages regardless of their status attr_accessor :filter_uptodate_packages # Override the osdeps mode def osdeps_mode=(value) @osdeps_mode = OSDependencies.osdeps_mode_string_to_value(value) end # Returns the osdeps mode chosen by the user def osdeps_mode # This has two uses. It caches the value extracted from the # AUTOPROJ_OSDEPS_MODE and/or configuration file. Moreover, it # allows to override the osdeps mode by using # OSDependencies#osdeps_mode= if @osdeps_mode return @osdeps_mode end @osdeps_mode = OSDependencies.osdeps_mode end def self.osdeps_mode while true mode = if !Autoproj.has_config_key?('osdeps_mode') && mode_name = ENV['AUTOPROJ_OSDEPS_MODE'] begin OSDependencies.osdeps_mode_string_to_value(mode_name) rescue ArgumentError Autoproj.warn "invalid osdeps mode given through AUTOPROJ_OSDEPS_MODE (#{mode})" nil end else mode_name = Autoproj.user_config('osdeps_mode') begin OSDependencies.osdeps_mode_string_to_value(mode_name) rescue ArgumentError Autoproj.warn "invalid osdeps mode stored in configuration file" nil end end if mode @osdeps_mode = mode Autoproj.change_option('osdeps_mode', mode_name, true) return mode end # Invalid configuration values. Retry Autoproj.reset_option('osdeps_mode') ENV['AUTOPROJ_OSDEPS_MODE'] = nil end end # The set of packages that have already been installed attr_reader :installed_packages def osdeps_interaction_unknown_os(osdeps) puts <<-EOMSG #{Autoproj.color("The build process requires some other software packages to be installed on our operating system", :bold)} #{Autoproj.color("If they are already installed, simply ignore this message", :red)} #{osdeps.join("\n ")} EOMSG print Autoproj.color("Press ENTER to continue", :bold) STDOUT.flush STDIN.readline puts nil end def installs_os_packages? osdeps_mode == HANDLE_ALL || osdeps_mode == HANDLE_OS end def installs_ruby_packages? osdeps_mode == HANDLE_ALL || osdeps_mode == HANDLE_RUBY end def osdeps_interaction(osdeps, os_packages, shell_script, silent) if !OSDependencies.supported_operating_system? if silent return false else return osdeps_interaction_unknown_os(osdeps) end elsif OSDependencies.force_osdeps return true elsif osdeps_mode == HANDLE_ALL || osdeps_mode == HANDLE_OS return true elsif silent return false end # We're asked to not install the OS packages but to display them # anyway, do so now puts <<-EOMSG #{Autoproj.color("The build process and/or the packages require some other software to be installed", :bold)} #{Autoproj.color("and you required autoproj to not install them itself", :bold)} #{Autoproj.color("\nIf these packages are already installed, simply ignore this message\n", :red) if !can_filter_uptodate_packages?} The following packages are available as OS dependencies, i.e. as prebuilt packages provided by your distribution / operating system. You will have to install them manually if they are not already installed #{os_packages.sort.join("\n ")} the following command line(s) can be run as root to install them: #{shell_script.split("\n").join("\n| ")} EOMSG print " #{Autoproj.color("Press ENTER to continue ", :bold)}" STDOUT.flush STDIN.readline puts false end def gems_interaction(gems, cmdlines, silent) if OSDependencies.force_osdeps return true elsif osdeps_mode == HANDLE_ALL || osdeps_mode == HANDLE_RUBY return true elsif silent return false end # We're not supposed to install rubygem packages but silent is not # set, so display information about them anyway puts <<-EOMSG #{Autoproj.color("The build process and/or the packages require some Ruby Gems to be installed", :bold)} #{Autoproj.color("and you required autoproj to not do it itself", :bold)} You can use the --all or --ruby options to autoproj osdeps to install these packages anyway, and/or change to the osdeps handling mode by running an autoproj operation with the --reconfigure option as for instance autoproj build --reconfigure The following command line can be used to install them manually #{cmdlines.map { |c| c.join(" ") }.join("\n ")} Autoproj expects these Gems to be installed in #{Autoproj.gem_home} This can be overridden by setting the AUTOPROJ_GEM_HOME environment variable manually EOMSG print " #{Autoproj.color("Press ENTER to continue ", :bold)}" STDOUT.flush STDIN.readline puts false end # Requests the installation of the given set of packages def install(packages, package_osdeps = Hash.new) handled_os = OSDependencies.supported_operating_system? # Remove the set of packages that have already been installed packages -= installed_packages return if packages.empty? osdeps, gems = partition_packages(packages) if handled_os os_names, _ = OSDependencies.operating_system os_packages = resolve_os_dependencies(osdeps) if filter_uptodate_packages os_packages = filter_uptodate_os_packages(os_packages, os_names) end end if filter_uptodate_packages gems = filter_uptodate_gems(gems) end did_something = false if !osdeps.empty? && (!os_packages || !os_packages.empty?) if handled_os shell_script = generate_auto_os_script(os_names, os_packages) user_shell_script = generate_user_os_script(os_names, os_packages) end if osdeps_interaction(osdeps, os_packages, user_shell_script, silent?) Autoproj.progress " installing OS packages: #{os_packages.sort.join(", ")}" if Autoproj.verbose Autoproj.progress "Generating installation script for non-ruby OS dependencies" Autoproj.progress shell_script end File.open('/tmp/autoproj_osdeps_lock', 'w') do |lock_io| begin while !lock_io.flock(File::LOCK_EX | File::LOCK_NB) Autoproj.progress " waiting for other autoproj instances to finish their osdeps installation" sleep 5 end Tempfile.open('osdeps_sh') do |io| io.puts "#! /bin/bash" io.puts GAIN_ROOT_ACCESS io.write shell_script io.flush Autobuild::Subprocess.run 'autoproj', 'osdeps', '/bin/bash', io.path end ensure lock_io.flock(File::LOCK_UN) end end did_something = true end end # Now install the RubyGems if !gems.empty? guess_gem_program base_cmdline = [Autobuild.tool('gem'), 'install'] if !OSDependencies.gem_doc base_cmdline << '--no-rdoc' << '--no-ri' end if Autoproj::OSDependencies.gem_with_prerelease base_cmdline << "--prerelease" end with_version, without_version = gems.partition { |name, v| v } cmdlines = [] if !without_version.empty? cmdlines << (base_cmdline + without_version.flatten) end with_version.each do |name, v| cmdlines << base_cmdline + [name, "-v", v] end if gems_interaction(gems, cmdlines, silent?) Autobuild.progress "installing/updating RubyGems dependencies: #{gems.map { |g| g.join(" ") }.sort.join(", ")}" cmdlines.each do |c| Autobuild::Subprocess.run 'autoproj', 'osdeps', *c end did_something = true end end did_something end end end module Autoproj class InputError < RuntimeError; end class BuildOption attr_reader :name attr_reader :type attr_reader :options attr_reader :validator TRUE_STRINGS = %w{on yes y true} FALSE_STRINGS = %w{off no n false} def initialize(name, type, options, validator) @name, @type, @options = name.to_str, type.to_str, options.to_hash @validator = validator.to_proc if validator if !BuildOption.respond_to?("validate_#{type}") raise ConfigError.new, "invalid option type #{type}" end end def short_doc if short_doc = options[:short_doc] short_doc elsif doc = options[:doc] if doc.respond_to?(:to_ary) then doc.first else doc end else "#{name} (no documentation for this option)" end end def doc doc = (options[:doc] || "#{name} (no documentation for this option)") if doc.respond_to?(:to_ary) # multi-line first_line = doc[0] remaining = doc[1..-1] if remaining.empty? first_line else remaining = remaining.join("\n").split("\n").join("\n ") Autoproj.color(first_line, :bold) + "\n " + remaining end else doc end end def ask(current_value, doc = nil) default_value = if current_value then current_value.to_s elsif options[:default] then options[:default].to_str else '' end STDOUT.print " #{doc || self.doc} [#{default_value}] " STDOUT.flush answer = STDIN.readline.chomp if answer == '' answer = default_value end validate(answer) rescue InputError => e Autoproj.progress("invalid value: #{e.message}", :red) retry end def validate(value) value = BuildOption.send("validate_#{type}", value, options) if validator value = validator[value] end value end def self.validate_boolean(value, options) if TRUE_STRINGS.include?(value.downcase) true elsif FALSE_STRINGS.include?(value.downcase) false else raise InputError, "invalid boolean value '#{value}', accepted values are '#{TRUE_STRINGS.join(", ")}' for true, and '#{FALSE_STRINGS.join(", ")} for false" end end def self.validate_string(value, options) if possible_values = options[:possible_values] if options[:lowercase] value = value.downcase elsif options[:uppercase] value = value.upcase end if !possible_values.include?(value) raise InputError, "invalid value '#{value}', accepted values are '#{possible_values.join("', '")}' (without the quotes)" end end value end end @user_config = Hash.new def self.option_set @user_config.inject(Hash.new) do |h, (k, v)| h[k] = v.first h end end def self.reset_option(key) @user_config.delete(key) end def self.change_option(key, value, user_validated = false) @user_config[key] = [value, user_validated] end def self.user_config(key) value, seen = @user_config[key] # All non-user options are always considered as "seen" seen ||= !@declared_options.has_key?(key) if value.nil? || (!seen && Autoproj.reconfigure?) value = configure(key) else if !seen doc = @declared_options[key].short_doc if doc[-1, 1] != "?" doc = "#{doc}:" end Autoproj.progress " #{doc} #{value}" @user_config[key] = [value, true] end value end end @declared_options = Hash.new def self.configuration_option(name, type, options, &validator) @declared_options[name] = BuildOption.new(name, type, options, validator) end def self.declared_option?(name) @declared_options.has_key?(name) end def self.configure(option_name) if opt = @declared_options[option_name] if current_value = @user_config[option_name] current_value = current_value.first end value = opt.ask(current_value) @user_config[option_name] = [value, true] value else raise ConfigError.new, "undeclared option '#{option_name}'" end end def self.save_config File.open(File.join(Autoproj.config_dir, "config.yml"), "w") do |io| config = Hash.new @user_config.each_key do |key| config[key] = @user_config[key].first end io.write YAML.dump(config) end end def self.has_config_key?(name) @user_config.has_key?(name) end def self.load_config config_file = File.join(Autoproj.config_dir, "config.yml") if File.exists?(config_file) config = YAML.load(File.read(config_file)) if !config return end config.each do |key, value| @user_config[key] = [value, false] end end end class << self attr_accessor :reconfigure end def self.reconfigure?; @reconfigure end end module Autoproj class UserError < RuntimeError; end # Returns true if +path+ is part of an autoproj installation def self.in_autoproj_installation?(path) root_dir(File.expand_path(path)) true rescue UserError false end # Returns the root directory of the current autoproj installation. # # If the current directory is not in an autoproj installation, # raises UserError. def self.root_dir(dir = Dir.pwd) while dir != "/" && !File.directory?(File.join(dir, "autoproj")) dir = File.dirname(dir) end if dir == "/" raise UserError, "not in a Autoproj installation" end dir end # Returns the configuration directory for this autoproj installation. # # If the current directory is not in an autoproj installation, # raises UserError. def self.config_dir File.join(root_dir, "autoproj") end class << self # The directory in which packages will be installed. # # If it is a relative path, it is relative to the root dir of the # installation. # # The default is "install" attr_reader :prefix # Change the value of 'prefix' def prefix=(new_path) @prefix = new_path Autoproj.change_option('prefix', new_path, true) end end @prefix = "install" # Returns the build directory (prefix) for this autoproj installation. # # If the current directory is not in an autoproj installation, raises # UserError. def self.build_dir File.expand_path(Autoproj.prefix, root_dir) end # Returns the path to the provided configuration file. # # If the current directory is not in an autoproj installation, raises # UserError. def self.config_file(file) File.join(config_dir, file) end # Run the provided command as user def self.run_as_user(*args) if !system(*args) raise "failed to run #{args.join(" ")}" end end # Run the provided command as root, using sudo to gain root access def self.run_as_root(*args) if !system('sudo', *args) raise "failed to run #{args.join(" ")} as root" end end # Return the directory in which remote package set definition should be # checked out def self.remotes_dir File.join(root_dir, ".remotes") end # Return the directory in which RubyGems package should be installed def self.gem_home ENV['AUTOPROJ_GEM_HOME'] || File.join(root_dir, ".gems") end # Find the given program in PATH. It raises ArgumentError if the program # can't be found def self.find_in_path(name) result = ENV['PATH'].split(':').find { |dir| File.file?(File.join(dir, name)) } if !result raise ArgumentError, "#{name} can not be found in PATH" end File.join(result, name) end # Initializes the environment variables to a "sane default" # # Use this in autoproj/init.rb to make sure that the environment will not # get polluted during the build. def self.set_initial_env Autoproj.env_set 'RUBYOPT', "-rubygems" Autoproj.env_set 'GEM_HOME', Autoproj.gem_home Autoproj.env_set_path 'PATH', "#{Autoproj.gem_home}/bin", "/usr/local/bin", "/usr/bin", "/bin" Autoproj.env_set 'PKG_CONFIG_PATH' Autoproj.env_set 'RUBYLIB' Autoproj.env_set 'LD_LIBRARY_PATH' end class << self attr_writer :shell_helpers def shell_helpers?; !!@shell_helpers end end @shell_helpers = true # Create the env.sh script in +subdir+. In general, +subdir+ should be nil. def self.export_env_sh(subdir = nil) filename = if subdir File.join(Autoproj.root_dir, subdir, "env.sh") else File.join(Autoproj.root_dir, "env.sh") end shell_dir = File.expand_path(File.join("..", "..", "shell"), File.dirname(__FILE__)) if Autoproj.shell_helpers? && shell = ENV['SHELL'] shell_kind = File.basename(shell) if shell_kind =~ /^\w+$/ shell_file = File.join(shell_dir, "autoproj_#{shell_kind}") if File.exists?(shell_file) Autoproj.progress Autoproj.progress "autodetected the shell to be #{shell_kind}, sourcing autoproj shell helpers" Autoproj.progress "add \"Autoproj.shell_helpers = false\" in autoproj/init.rb to disable" Autobuild.env_source_file(shell_file) end end end File.open(filename, "w") do |io| Autobuild.export_env_sh(io) end end # Load a definition file given at +path+. +source+ is the package set from # which the file is taken. # # If any error is detected, the backtrace will be filtered so that it is # easier to understand by the user. Moreover, if +source+ is non-nil, the # package set name will be mentionned. def self.load(package_set, *path) path = File.join(*path) in_package_set(package_set, File.expand_path(path).gsub(/^#{Regexp.quote(Autoproj.root_dir)}\//, '')) do begin Kernel.load path rescue Interrupt raise rescue ConfigError => e raise rescue Exception => e filter_load_exception(e, package_set, path) end end end # Same as #load, but runs only if the file exists. def self.load_if_present(package_set, *path) path = File.join(*path) if File.file?(path) self.load(package_set, *path) end end # Look into +dir+, searching for shared libraries. For each library, display # a warning message if this library has undefined symbols. def self.validate_solib_dependencies(dir, exclude_paths = []) Find.find(File.expand_path(dir)) do |name| next unless name =~ /\.so$/ next if exclude_paths.find { |p| name =~ p } output = `ldd -r #{name} 2>&1` if output =~ /undefined symbol/ Autoproj.progress(" WARN: #{name} has undefined symbols", :magenta) end end end end # Override Autoproj.root_dir module Autoproj def self.root_dir @root_dir end @root_dir = Dir.pwd end DEFS = < e STDERR.puts "failed: #{e.message}" exit(1) end # Now try to find out the name of the gem binary PACKAGES = %w{lsb_release} ENV['RUBYOPT'] = "-rubygems" require 'rubygems' STDERR.puts "autoproj: installing autoproj and its dependencies (this can take a long time)" # First install the dependencies of autoproj, as we don't want them to be # affected by the prerelease flag begin osdeps_management.install(PACKAGES) rescue Autoproj::ConfigError => e STDERR.puts "failed: #{e.message}" exit(1) end File.open('env.sh', 'w') do |io| io.write <<-EOSHELL export RUBYOPT=-rubygems export GEM_HOME=#{needed_gem_home} export PATH=$GEM_HOME/bin:$PATH EOSHELL end # If the user specifies "dev" on the command line, install the prerelease # version of autoproj. If it is "localdev", expect him to install autoproj and # run autoproj bootstrap manually. if ARGV.first != "localdev" if ARGV.first == "dev" Autoproj::OSDependencies.gem_with_prerelease = true ARGV.shift end begin osdeps_management.install(['autobuild']) osdeps_management.install(['autoproj']) rescue Autoproj::ConfigError => e STDERR.puts "failed: #{e.message}" exit(1) end Autoproj::OSDependencies.gem_with_prerelease = false if !system('autoproj', 'bootstrap', *ARGV) STDERR.puts "ERROR: failed to run autoproj bootstrap #{ARGV.join(", ")}" exit 1 end end