#! /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 all_env_sh = Array.new Autobuild::Reporting << Autoproj::Reporter.new def color(*args) Autoproj.console.color(*args) end def do_status(packages) console = Autoproj.console Autobuild::Reporting.report do last_was_in_sync = false packages.sort.each do |pkg| pkg = Autobuild::Package[pkg] if !pkg.importer.respond_to?(:status) # This importer does not support status display STDERR.puts color(" this package's importer does not support status display", :bold, :red) next end lines = [] 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.name}" else STDERR.print pkg.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 if last_was_in_sync STDERR.puts color(": local and remote are in sync", :green) end last_was_in_sync = false STDERR.print "#{pkg.name}:" 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 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 Autobuild::Reporting.report do osdeps = Autoproj::OSDependencies.load_default osdeps.install([vcs.type]) Autoproj::Manifest.import_whole_installation(vcs, File.join(Dir.pwd, "autoproj")) end # 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 < layout_name for all packages that # have already been handled handled_packages = Hash.new Autoproj.manifest.each_package_set(selected_packages) do |name, packages, enabled_packages| packages -= handled_packages.keys.to_set enabled_packages -= handled_packages.keys.to_set # Here, 'packages' are the packages listed in the layout configuration, # and 'enabled_packages' the ones that # # They do not (yet) take into account dependency information -- this is # not doable as the manifests have not been loaded yet (the packages # aren't imported). We add this information later on # Setup directories 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 # We are doing a status, now is the right time. Otherwise, build ! But # only if there is some packages to build (and avoid display of progress # messages if there is nothing to build) all_enabled_packages = Set.new STDERR.puts if only_do_status STDERR.puts color("autoproj: status of packages in #{name}", :bold) do_status(enabled_packages) elsif !enabled_packages.empty? Autobuild::Reporting.report do STDERR.puts STDERR.puts color("autoproj: now building #{name}", :bold) STDERR.puts color(" updating packages", :bold) # We actually have a problem there: the packages in # enabled_packages are *not* taking into account dependencies as # we can't load the manifests yet ... # # So we have to make our own dependency analysis. Fortunately # (for us), there is no need to import packages in order, so we # can do a simple BFS # # Additional benefit: it computes the set of packages packages_to_import = enabled_packages.dup while !packages_to_import.empty? import_now, packages_to_import = packages_to_import, Set.new import_now.each do |pkg_name| # Already handled at another place in the layout next if handled_packages.has_key?(pkg_name) # Already handled in this part of the layout next if all_enabled_packages.include?(pkg_name) # Not handled already. Import, load the manifest, add to # all_enabled_packages and add the dependencies Rake::Task["#{pkg_name}-import"].invoke manifest.load_package_manifest(pkg_name) all_enabled_packages << pkg_name Autobuild::Package[pkg_name].dependencies.each do |dep_name| if !handled_packages.has_key?(dep_name) && !packages.include?(dep_name) raise ConfigError, "#{pkg_name}, at #{name} in the layout, depends on #{dep_name} but this package appears in the layout neither at #{name} nor before" end packages_to_import << dep_name end end end if !no_os_deps STDERR.puts color(" installing prepackaged dependencies", :bold) manifest.install_os_dependencies(all_enabled_packages) end # And now build if Autobuild.only_doc STDERR.puts color(" building and installing documentation", :bold) else STDERR.puts color(" building and installing packages", :bold) end Autobuild.apply(all_enabled_packages, "autoproj-#{name}") Autobuild::Reporting.success end end # Now mark them as handled packages.each do |pkg_name| handled_packages[pkg_name] = name end # Now call the prepare target for all packages as it may be useful for # the rest of the builds and for the generation of the env.sh file # # Note that we don't really have to care about dependencies, but we do # it anyway old_update_flag = Autobuild.do_update begin Autobuild.do_update = false Autobuild::Reporting.report do prepare_targets = (packages - enabled_packages). find_all { |pkg_name| File.directory?(Autobuild::Package[pkg_name].srcdir) }. map { |pkg_name| "#{pkg_name}-prepare" } task "autoproj-#{name}-prepare" => prepare_targets Rake::Task["autoproj-#{name}-prepare"].invoke end ensure Autobuild.do_update = old_update_flag end if (mode == "build" || mode == "fast-build") if packages.all? { |pkg_name| File.directory?(Autobuild::Package[pkg_name].srcdir) } all_env_sh << name Autoproj.export_env_sh(name) else STDERR.puts color("WARN: #{name} has not been completely built, #{name}env.sh is not updated", :magenta) end end libdir = File.join(prefix, "lib") if File.directory?(libdir) Autoproj.validate_solib_dependencies(libdir) end end if !all_env_sh.empty? STDERR.puts <<-EOTEXT add the following lines at the bottom of your .bashrc: #{all_env_sh.map { |name| "source #{Dir.pwd}#{name}env.sh" }.join("\n ")} WARNING: autoproj will not work until your restart all your consoles, or run the following in them: #{all_env_sh.map { |name| "$ source #{Dir.pwd}#{name}env.sh" }.join("\n ")} EOTEXT end rescue ConfigError => 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