#! /usr/bin/env ruby if RUBY_VERSION < "1.8.7" STDERR.puts "autoproj requires Ruby >= 1.8.7" exit 1 end require 'autoproj' require 'autoproj/autobuild' require 'open-uri' require 'highline' include Autoproj InputError = Autoproj::InputError module Autoproj @verbose = false @console = HighLine.new class << self attr_accessor :verbose attr_reader :console end end # Some configuration variables mail_config = Hash.new only_update_sources = false only_do_status = false no_os_deps = false debug = false # Parse the configuration options parser = OptionParser.new do |opts| opts.banner = <<-EOBANNER autoproj mode [options] where 'mode' is one of: build: import, build and install all packages that need it. A package or package set name can be given, in which case only this package and its dependencies will be taken into account. Example: autoproj build drivers/hokuyo force-build: triggers all build commands, i.e. don't be lazy like in "build" fast-build: builds without updating and without considering OS dependencies rebuild: clean and then rebuild doc: generate and install documentation for packages that have some update: only import/update packages, do not build them status: displays the state of the packages w.r.t. their source VCS bootstrap: starts a new autoproj installation. Usage: autoproj bootstrap [manifest_url|source_vcs source_url opt1=value1 opt2=value2 ...] list-sets: list all available package sets update-sets: update the package sets definitions switch-config: change where the configuration should be taken from. Syntax: autoproj switch-config source_vcs source_url opt1=value1 opt2=value2 ... For example: autoproj switch-config git git://github.com/doudou/rubim-all.git branch=all Additional options: EOBANNER opts.on("--reconfigure", "re-ask all configuration options (build mode only)") do Autoproj.reconfigure = true end opts.on("--no-update", "do not update already checked-out packages (build mode only)") do Autobuild.do_update = false end opts.on("--no-osdeps", "do not install prepackaged dependencies") do no_os_deps = true end opts.on("--verbose", "verbose output") do Autoproj.verbose = true Autobuild.verbose = true Rake.application.options.trace = false end opts.on("--debug", "debugging output") do Autoproj.verbose = true Autobuild.verbose = true Rake.application.options.trace = true debug = true end opts.on('--nice NICE', Integer, 'nice the subprocesses to the given value') do |value| Autobuild.nice = value end opts.on("-h", "--help", "Show this message") do puts opts exit end opts.on("--mail-from EMAIL", String, "From: field of the sent mails") do |from_email| mail_config[:from] = from_email end opts.on("--mail-to EMAILS", String, "comma-separated list of emails to which the reports should be sent") do |emails| mail_config[:to] ||= [] mail_config[:to] += emails.split(',') end opts.on("--mail-subject SUBJECT", String, "Subject: field of the sent mails") do |subject_email| mail_config[:subject] = subject_email end opts.on("--mail-smtp HOSTNAME", String, " address of the mail server written as hostname[:port]") do |smtp| raise "invalid SMTP specification #{smtp}" unless smtp =~ /^([^:]+)(?::(\d+))?$/ mail_config[:smtp] = $1 mail_config[:port] = Integer($2) if $2 && !$2.empty? end opts.on("--mail-only-errors", "send mail only on errors") do mail_config[:only_errors] = true end end args = ARGV.dup parser.parse!(args) mode = args.shift selected_packages = args.dup Autobuild::Reporting << Autoproj::Reporter.new def color(*args) Autoproj.console.color(*args) end def display_status(packages) last_was_in_sync = false packages.each do |pkg| lines = [] if !pkg.importer.respond_to?(:status) # This importer does not support status display STDERR.puts color(" the importer of #{pkg.autoproj_name} does not support status display (#{pkg.importer.class.name})", :bold, :red) next end if !File.directory?(pkg.srcdir) lines << color(" is not imported yet", :magenta) else status = pkg.importer.status(pkg) if status.uncommitted_code lines << color(" contains uncommitted modifications", :red) end case status.status when Autobuild::Importer::Status::UP_TO_DATE if !status.uncommitted_code if last_was_in_sync STDERR.print ", #{pkg.autoproj_name}" else STDERR.print pkg.autoproj_name end last_was_in_sync = true next else lines << color(" local and remote are in sync", :green) end when Autobuild::Importer::Status::ADVANCED lines << color(" local contains #{status.local_commits.size} commit that remote does not have:", :magenta) status.local_commits.each do |line| lines << color(" #{line}", :magenta) end when Autobuild::Importer::Status::SIMPLE_UPDATE lines << color(" remote contains #{status.remote_commits.size} commit that local does not have:", :magenta) status.remote_commits.each do |line| lines << color(" #{line}", :magenta) end when Autobuild::Importer::Status::NEEDS_MERGE lines << color(" local and remote have diverged with respectively #{status.local_commits.size} and #{status.remote_commits.size} commits each", :magenta) lines << " -- local commits --" status.local_commits.each do |line| lines << color(" #{line}", :magenta) end lines << " -- remote commits --" status.remote_commits.each do |line| lines << color(" #{line}", :magenta) end end end if last_was_in_sync STDERR.puts color(": local and remote are in sync", :green) end last_was_in_sync = false if pkg.respond_to?(:text_name) STDERR.print "#{pkg.text_name}:" else STDERR.print "#{pkg.autoproj_name}:" end if lines.size == 1 STDERR.puts lines.first else STDERR.puts STDERR.puts lines.join("\n") end end if last_was_in_sync STDERR.puts color(": local and remote are in sync", :green) end end def do_status(packages) console = Autoproj.console sources = Autoproj.manifest.each_configuration_source. map do |vcs, text_name, pkg_name, local_dir| Autoproj::Manifest.create_autobuild_package(vcs, text_name, pkg_name, local_dir) end if !sources.empty? STDERR.puts color("autoproj: displaying status of configuration", :bold) display_status(sources) STDERR.puts end STDERR.puts color("autoproj: displaying status of packages", :bold) packages = packages.sort.map do |pkg_name| Autobuild::Package[pkg_name] end display_status(packages) end def switch_config(*args) Autoproj.load_config if Autoproj.has_config_key?('manifest_source') vcs = Autoproj.normalize_vcs_definition(Autoproj.user_config('manifest_source')) end if vcs && (vcs.type == args[0] && vcs.url == args[1]) # Don't need to do much: simply change the options and save the config # file, the VCS handler will take care of switching the options else # We will have to delete the current autoproj directory. Ask the user. opt = Autoproj::BuildOption.new("delete current config", "boolean", Hash[:default => "false", :doc => "delete the current configuration ? (required to switch)"], nil) return if !opt.ask(nil) FileUtils.rm_rf File.join(Autoproj.config_dir) do_switch_config(*args) end # And now save the options: note that we keep the current option set even # though we switched configuration. This is not a problem as undefined # options will not be reused # # TODO: cleanup the options to only keep the relevant ones vcs_def = Hash['type' => args.shift, 'url' => args.shift] args.each do |opt| opt_name, opt_val = opt.split '=' vcs_def[opt_name] = opt_val end # Validate the option hash, just in case Autoproj.normalize_vcs_definition(vcs_def) # Save the new options Autoproj.change_option('manifest_source', vcs_def, true) Autoproj.save_config end def do_switch_config(*args) vcs_def = Hash.new vcs_def[:type] = args.shift vcs_def[:url] = args.shift while !args.empty? name, value = args.shift.split("=") vcs_def[name] = value end vcs = Autoproj.normalize_vcs_definition(vcs_def) # Install the OS dependencies required for this VCS osdeps = Autoproj::OSDependencies.load_default osdeps.install([vcs.type]) Autoproj::Manifest.update_source(vcs, "autoproj main configuration", 'autoproj_config', File.join(Dir.pwd, "autoproj")) # Now write it in the config file File.open(File.join(Autoproj.config_dir, "config.yml"), "a") do |io| io.puts <<-EOTEXT manifest_source: type: #{vcs_def.delete(:type)} url: #{vcs_def.delete(:url)} #{vcs_def.map { |k, v| "#{k}: #{v}" }.join("\n ")} EOTEXT end end def do_bootstrap(*args) if File.exists?(File.join("autoproj", "manifest")) raise ConfigError, "this installation is already bootstrapped. Remove the autoproj directory if it is not the case" end Autobuild.logdir = File.join('build', 'log') # Check if we are being called from another GEM_HOME. If it is the case, # assume that we are bootstrapping from another installation directory and # start by copying the .gems directory if ENV['GEM_HOME'] && ENV['GEM_HOME'] =~ /\.gems\/?$/ && ENV['GEM_HOME'] != File.join(Dir.pwd, ".gems") STDERR.puts "autoproj: reusing bootstrap from #{File.dirname(ENV['GEM_HOME'])}" FileUtils.cp_r ENV['GEM_HOME'], ".gems" ENV['GEM_HOME'] = File.join(Dir.pwd, ".gems") exec $0, *ARGV end # If we are not getting the installation setup from a VCS, copy the template # files if args.empty? || args.size == 1 sample_dir = File.expand_path(File.join("..", "samples"), File.dirname(__FILE__)) FileUtils.cp_r File.join(sample_dir, "autoproj"), "autoproj" end if args.size == 1 # the user asks us to download a manifest manifest_url = args.first STDERR.puts color("autoproj: downloading manifest file #{manifest_url}", :bold) manifest_data = begin open(manifest_url) { |file| file.read } rescue raise ConfigError, "cannot read #{manifest_url}, did you mean 'autoproj bootstrap VCSTYPE #{manifest_url}' ?" end File.open(File.join(Autoproj.config_dir, "manifest"), "w") do |io| io.write(manifest_data) end elsif args.size >= 2 # is a VCS definition for the manifest itself ... do_switch_config(*args) end # Finally, generate an env.sh script File.open('env.sh', 'w') do |io| io.write <<-EOSHELL export RUBYOPT=-rubygems export GEM_HOME=#{Dir.pwd}/.gems export PATH=$GEM_HOME/bin:$PATH EOSHELL end STDERR.puts < e STDERR.puts STDERR.puts color(e.message, :red, :bold) if debug then raise else exit 1 end rescue Interrupt STDERR.puts STDERR.puts color("Interrupted by user", :red, :bold) if debug then raise else exit 1 end end Autobuild.doc_errors = false Autobuild.do_doc = false # Find the autoproj root dir report(debug) do case mode when "bootstrap" # If there is no argument, We need one more argument. It is either a VCS type or a path to a # manifest. # # In the first case, we create a new manifest and add the source given on # the command line to it. In the second case, we simply use it as a # manifest. do_bootstrap(*args) if args.empty? # no package sets defined, we're done exit(0) end only_update_sources = true when "build" when "force-build" Autobuild.do_forced_build = true when "rebuild" Autobuild.do_rebuild = true when "fast-build" Autobuild.do_update = false no_os_deps = true when "update" Autobuild.do_build = false when "status" only_do_status = true Autobuild.do_update = false no_os_deps = true when "update-sets" only_update_sources = true when "list-sets" only_update_sources = true Autobuild.do_update = false when "switch-config" if switch_config(*args) exit 0 else exit 1 end when "doc" Autobuild.do_update = false Autobuild.do_doc = true Autobuild.only_doc = true else puts parser exit(1) end # Expand directories in the selected_packages set root_dir = Autoproj.root_dir selected_packages.map! do |name| if File.directory?(name) File.expand_path(name).gsub(/^#{Regexp.quote(root_dir)}/, '') else name end end Dir.chdir(root_dir) # Load user configuration Autoproj.load_config # If we are under rubygems, check that the GEM_HOME is right ... if $LOADED_FEATURES.any? { |l| l =~ /rubygems/ } if ENV['GEM_HOME'] != Autoproj.gem_home raise ConfigError, "RubyGems is already loaded with a different GEM_HOME, make sure you are loading the right env.sh script !" end end # Set up some important autobuild parameters Autoproj.env_inherit 'PATH', 'PKG_CONFIG_PATH', 'RUBYLIB', 'LD_LIBRARY_PATH' Autoproj.env_set 'GEM_HOME', Autoproj.gem_home Autoproj.env_add 'PATH', File.join(Autoproj.gem_home, 'bin') Autoproj.env_set 'RUBYOPT', "-rubygems" Autobuild.prefix = Autoproj.build_dir Autobuild.srcdir = root_dir Autobuild.logdir = File.join(Autobuild.prefix, 'log') if mail_config[:to] Autobuild::Reporting << Autobuild::MailReporter.new(mail_config) end # First things first, see if we need to update ourselves osdeps = Autoproj::OSDependencies.load_default if osdeps.install(%w{autobuild autoproj}) # We updated autobuild or autoproj themselves ... Restart ! exec($0, *ARGV) end manifest_path = File.join(Autoproj.config_dir, 'manifest') # Load the installation's manifest a first time, to check if we should # update it ... We assume that the OS dependencies for this VCS is already # installed (i.e. that the user did not remove it) manifest = Manifest.load(manifest_path) if manifest.vcs && mode != "bootstrap" manifest.update_yourself manifest = Manifest.load(manifest_path) end Autoproj.manifest = manifest source_os_dependencies = manifest.each_remote_source(false). inject(Set.new) do |set, source| set << source.vcs.type if !source.local? end # Update the remote sources if there are any if manifest.has_remote_sources? STDERR.puts color("autoproj: updating remote definitions of package sets", :bold) # If we need to install some packages to import our remote sources, do it if !no_os_deps osdeps = manifest.known_os_packages osdeps.install(source_os_dependencies) end manifest.update_remote_sources STDERR.puts end # Load init.rb files. each_source must not load the source.yml file, as # init.rb may define configuration options that are used there manifest.each_source(false) do |source| Autoproj.load_if_present(source, source.local_dir, "init.rb") end # Load the required autobuild definitions STDERR.puts color("autoproj: loading ...", :bold) if !Autoproj.reconfigure? STDERR.puts color("run 'autoproj --reconfigure' to change configuration values", :bold) end manifest.each_autobuild_file do |source, name| Autoproj.import_autobuild_file source, name end # Load the package's override files. each_source must not load the # source.yml file, as init.rb may define configuration options that are used # there manifest.each_source(false).to_a.reverse.each do |source| Autoproj.load_if_present(source, source.local_dir, "overrides.rb") end # The user is asked for configuration values both during the manifest # loading and the loading of autobuild files. Save it now. Autoproj.save_config # Now, load the package's importer configurations (from the various # source.yml files) manifest.load_importers if Autoproj.verbose # List defined packages, and in which autobuild files they are defined STDERR.puts "Available packages:" Autoproj.manifest.packages.each_value do |package, source, file| STDERR.puts " #{package.name}: #{file} from #{source.name}" end end # If in verbose mode, or if we only update sources, list the sources # # Note that we can't have the Manifest class load the source.yml file, as it # cannot resolve all constants. So we need to do it ourselves to get the # name ... if Autoproj.verbose || only_update_sources sources = manifest.each_source(false).to_a if sources.empty? STDERR.puts color("autoproj: no package sets defined in autoproj/manifest", :bold, :red) else STDERR.puts color("autoproj: available package sets", :bold) manifest.each_source(false) do |source| source_yml = source.raw_description_file STDERR.puts " #{source_yml['name']}" if source.local? STDERR.puts " local source in #{source.local_dir}" else STDERR.puts " from: #{source.vcs}" STDERR.puts " local: #{source.local_dir}" end lines = [] source.each_package. map { |pkg| [pkg.name, Autoproj.manifest.package_manifests[pkg.name]] }. sort_by { |name, _| name }. each do |name, manifest| vcs_def = Autoproj.manifest.importer_definition_for(name) if manifest lines << [name, manifest.short_documentation] lines << ["", vcs_def.to_s] else lines << [name, vcs_def.to_s] end end w_col1, w_col2 = nil lines.each do |col1, col2| w_col1 = col1.size if !w_col1 || col1.size > w_col1 w_col2 = col2.size if !w_col2 || col2.size > w_col2 end STDERR.puts " packages:" format = " | %-#{w_col1}s | %-#{w_col2}s |" lines.each do |col1, col2| STDERR.puts(format % [col1, col2]) end end end end if only_update_sources exit(0) end # Create the build target from the manifest if the user did not provide an # explicit one if selected_packages.empty? selected_packages = Autoproj.manifest.default_packages end if Autoproj.verbose STDERR.puts "will install #{selected_packages.to_a.join(", ")}" end if selected_packages.empty? # no packages, terminate STDERR.puts STDERR.puts color("autoproj: no packages defined", :red) exit 0 end selected_packages = selected_packages.to_set # Now starts a different stage of the whole build. Until now, we were # working on the whole package set. Starting from now, we need to build the # package sets based on the layout file # # First, we allow to user to specify packages based on disk paths, so # resolve those selected_packages = Autoproj.manifest.expand_package_selection(selected_packages) seen = Set.new Autoproj.manifest.each_package_set do |name, packages, enabled_packages| packages -= seen srcdir = File.join(Autoproj.root_dir, name) prefix = File.join(Autoproj.build_dir, name) logdir = File.join(prefix, "log") Autobuild.logdir = logdir packages.each do |pkg_name| pkg = Autobuild::Package[pkg_name] pkg.srcdir = File.join(srcdir, pkg_name) pkg.prefix = prefix pkg.logdir = logdir end seen |= packages end if only_do_status STDERR.puts all_packages = Set.new Autoproj.manifest.handle_enabled_packages(selected_packages) do |name, _, layout_enabled| all_packages |= layout_enabled end do_status(all_packages) exit(0) end STDERR.puts STDERR.puts color("autoproj: importing and loading selected packages", :bold) # Install prepackaged dependencies needed to import and build the packages if !no_os_deps osdeps = manifest.known_os_packages osdeps.install(Autoproj.build_system_dependencies - source_os_dependencies) end # First thing: do the import. We are handling the imports ourselves as it # allows us to complete the enabled_packages set with the package # dependencies. The scheme is as follows: # * we import a package, prepare it and load its manifest # * we look at its dependencies. Dependencies are either already handled # (and nothing needs to be done) or are to-be-handled at this level. # Otherwise, it is considered as an error # seen = Set.new Autoproj.manifest.each_package_set do |name, packages, enabled_packages| packages -= seen old_update_flag = Autobuild.do_update begin Autobuild.do_update = false packages.each do |pkg_name| pkg = Autobuild::Package[pkg_name] if File.directory?(pkg.srcdir) Rake::Task["#{pkg_name}-import"].invoke end end ensure Autobuild.do_update = old_update_flag end seen |= packages end all_packages = Set.new all_enabled_packages = Set.new all_sublayouts = Set.new Autoproj.manifest.handle_enabled_packages(selected_packages) do |name, packages, enabled_packages, _| packages -= all_enabled_packages enabled_packages -= all_enabled_packages all_sublayouts << name packages_to_import = enabled_packages.dup.to_set while !packages_to_import.empty? import_now, packages_to_import = packages_to_import, Set.new import_now.sort.each do |pkg_name| # Already handled in this part of the layout next if all_enabled_packages.include?(pkg_name) # Not handled already. Import. Autobuild::Package[pkg_name].import manifest.load_package_manifest(pkg_name) all_enabled_packages << pkg_name # Add its dependencies to the next import set Autobuild::Package[pkg_name].dependencies.each do |dep_name| if !Autobuild::Package[dep_name] raise ConfigError, "#{pkg_name} depends on #{dep_name}, but it does not seem to exist" elsif all_enabled_packages.include?(dep_name) next end packages_to_import << dep_name end end all_packages.merge(packages) end end old_update_flag = Autobuild.do_update begin Autobuild.do_update = false prepare_targets = all_packages. find_all { |pkg_name| File.directory?(Autobuild::Package[pkg_name].srcdir) }. map { |pkg_name| "#{pkg_name}-prepare" } task "autoproj-prepare" => prepare_targets Rake::Task["autoproj-prepare"].invoke ensure Autobuild.do_update = old_update_flag end missing_packages = all_packages.find_all { |pkg_name| !File.directory?(Autobuild::Package[pkg_name].srcdir) } if missing_packages.empty? # Backward compatibility: check if name/env.sh exists, and if it is # the case, simply replace it with a link to the root one. And issue # a warning all_sublayouts.each do |name| env_file = File.join(Autoproj.root_dir, name, "env.sh") if name != '/' && File.file?(env_file) File.open(env_file, 'w') do |io| io.puts "source #{Autoproj.root_dir}/env.sh" end end end Autoproj.export_env_sh STDERR.puts color("autoproj: updated #{Autoproj.root_dir}/env.sh", :green) else STDERR.puts color("autoproj: #{missing_packages.join(", ")} are not yet imported, #{Autoproj.root_dir}/env.sh might not be up to date", :red) end if all_enabled_packages.empty? STDERR.puts color("autoproj: nothing to do", :bold) elsif Autobuild.do_build if Autobuild.only_doc STDERR.puts color("autoproj: building and installing documentation", :bold) else STDERR.puts color("autoproj: building and installing packages", :bold) end if !no_os_deps manifest.install_os_dependencies(all_enabled_packages) end Autobuild.apply(all_enabled_packages, "autoproj-build") end prefixes = all_enabled_packages.inject(Set.new) do |set, pkg_name| set << Autobuild::Package[pkg_name].prefix end prefixes.each do |prefix| libdir = File.join(prefix, "lib") if File.directory?(libdir) Autoproj.validate_solib_dependencies(libdir) end end end