lib/capistrano-maven.rb in capistrano-maven-0.0.7 vs lib/capistrano-maven.rb in capistrano-maven-0.1.0
- old
+ new
@@ -7,279 +7,341 @@
module Capistrano
module Maven
def self.extended(configuration)
configuration.load {
namespace(:mvn) {
- _cset(:mvn_version, '3.0.4')
- _cset(:mvn_major_version) {
- mvn_version.split('.').first.to_i
- }
+ _cset(:mvn_roles, [:app])
+ _cset(:mvn_version, "3.0.5")
+ _cset(:mvn_major_version) { mvn_version.split(".").first.to_i }
_cset(:mvn_archive_url) {
"http://www.apache.org/dist/maven/maven-#{mvn_major_version}/#{mvn_version}/binaries/apache-maven-#{mvn_version}-bin.tar.gz"
}
- _cset(:mvn_archive_file) {
- File.join(shared_path, 'tools', 'mvn', File.basename(URI.parse(mvn_archive_url).path))
- }
- _cset(:mvn_archive_file_local) {
- File.join(File.expand_path('.'), 'tools', 'mvn', File.basename(URI.parse(mvn_archive_url).path))
- }
- _cset(:mvn_checksum_url) {
- "#{mvn_archive_url}.md5"
- }
- _cset(:mvn_checksum_file) {
- File.join(shared_path, 'tools', 'mvn', File.basename(URI.parse(mvn_checksum_url).path))
- }
- _cset(:mvn_checksum_file_local) {
- File.join(File.expand_path('.'), 'tools', 'mvn', File.basename(URI.parse(mvn_checksum_url).path))
- }
- _cset(:mvn_checksum_cmd) {
- case File.extname(File.basename(URI.parse(mvn_checksum_url).path))
- when '.md5' then 'md5sum'
- when '.sha1' then 'sha1sum'
+ _cset(:mvn_tools_path) { File.join(shared_path, "tools", "mvn") }
+ _cset(:mvn_tools_path_local) { File.expand_path("tools/mvn") }
+ _cset(:mvn_archive_path) { mvn_tools_path }
+ _cset(:mvn_archive_path_local) { mvn_tools_path_local }
+ _cset(:mvn_archive_file) { File.join(mvn_archive_path, File.basename(URI.parse(mvn_archive_url).path)) }
+ _cset(:mvn_archive_file_local) { File.join(mvn_archive_path_local, File.basename(URI.parse(mvn_archive_url).path)) }
+ _cset(:mvn_path) { File.join(mvn_tools_path, File.basename(URI.parse(mvn_archive_url).path, "-bin.tar.gz")) }
+ _cset(:mvn_path_local) { File.join(mvn_tools_path_local, File.basename(URI.parse(mvn_archive_url).path, "-bin.tar.gz")) }
+ _cset(:mvn_bin_path) { File.join(mvn_path, "bin") }
+ _cset(:mvn_bin_path_local) { File.join(mvn_path_local, "bin") }
+ _cset(:mvn_bin) { File.join(mvn_bin_path, "mvn") }
+ _cset(:mvn_bin_local) { File.join(mvn_bin_path_local, "mvn") }
+ _cset(:mvn_project_path) { release_path }
+ _cset(:mvn_project_path_local) { File.expand_path(".") }
+ _cset(:mvn_template_path) { File.expand_path("config/templates") }
+
+ ## Maven environment
+ _cset(:mvn_common_environment, {})
+ _cset(:mvn_default_environment) {
+ environment = {}
+ environment["JAVA_HOME"] = fetch(:mvn_java_home) if exists?(:mvn_java_home)
+ if exists?(:mvn_java_options)
+ environment["MAVEN_OPTS"] = [ fetch(:mvn_java_options, []) ].flatten.join(" ")
end
+ environment["PATH"] = [ mvn_bin_path, "$PATH" ].join(":") if mvn_setup_remotely
+ _merge_environment(mvn_common_environment, environment)
}
- _cset(:mvn_path) {
- File.join(shared_path, 'tools', 'mvn', File.basename(URI.parse(mvn_archive_url).path, "-bin.tar.gz"))
- }
- _cset(:mvn_path_local) {
- File.join(File.expand_path('.'), 'tools', 'mvn', File.basename(URI.parse(mvn_archive_url).path, "-bin.tar.gz"))
- }
- _cset(:mvn_bin) {
- File.join(mvn_path, 'bin', 'mvn')
- }
- _cset(:mvn_bin_local) {
- File.join(mvn_path_local, 'bin', 'mvn')
- }
- _cset(:mvn_cmd) {
- if fetch(:mvn_java_home, nil)
- "env JAVA_HOME=#{mvn_java_home} #{mvn_bin} #{mvn_options.join(' ')}"
- else
- "#{mvn_bin} #{mvn_options.join(' ')}"
+ _cset(:mvn_default_environment_local) {
+ environment = {}
+ environment["JAVA_HOME"] = fetch(:mvn_java_home_local) if exists?(:mvn_java_home_local)
+ if exists?(:mvn_java_options_local)
+ environment["MAVEN_OPTS"] = [ fetch(:mvn_java_options_local, []) ].flatten.join(" ")
end
+ environment["PATH"] = [ mvn_bin_path_local, "$PATH" ].join(":") if mvn_setup_locally
+ _merge_environment(mvn_common_environment, environment)
}
- _cset(:mvn_cmd_local) {
- if fetch(:mvn_java_home_local, nil)
- "env JAVA_HOME=#{mvn_java_home_local} #{mvn_bin_local} #{mvn_options_local.join(' ')}"
+ _cset(:mvn_environment) { _merge_environment(mvn_default_environment, fetch(:mvn_extra_environment, {})) }
+ _cset(:mvn_environment_local) { _merge_environment(mvn_default_environment_local, fetch(:mvn_extra_environment_local, {})) }
+ def _command(cmdline, options={})
+ environment = options.fetch(:env, {})
+ if environment.empty?
+ cmdline
else
- "#{mvn_bin_local} #{mvn_options_local.join(' ')}"
+ env = (["env"] + environment.map { |k, v| "#{k}=#{v.dump}" }).join(" ")
+ "#{env} #{cmdline}"
end
- }
- _cset(:mvn_project_path) {
- release_path
- }
- _cset(:mvn_project_path_local) {
- Dir.pwd
- }
- _cset(:mvn_target_path) {
- File.join(mvn_project_path, 'target')
- }
- _cset(:mvn_target_path_local) {
- File.join(mvn_project_path_local, File.basename(mvn_target_path))
- }
- _cset(:mvn_template_path, File.join(File.dirname(__FILE__), 'templates'))
- _cset(:mvn_update_settings, false)
- _cset(:mvn_update_settings_locally, false)
- _cset(:mvn_settings_path) { mvn_project_path }
- _cset(:mvn_settings_path_local) { mvn_project_path_local }
- _cset(:mvn_settings, %w(settings.xml))
- _cset(:mvn_settings_local, %w(settings.xml))
- _cset(:mvn_cleanup_settings, [])
- _cset(:mvn_cleanup_settings_local, [])
- _cset(:mvn_compile_locally, false) # perform precompilation on localhost
+ end
+ def command(cmdline, options={})
+ _command(cmdline, :env => mvn_environment.merge(options.fetch(:env, {})))
+ end
+ def command_local(cmdline, options={})
+ _command(cmdline, :env => mvn_environment_local.merge(options.fetch(:env, {})))
+ end
+ _cset(:mvn_cmd) { command("#{mvn_bin.dump} #{mvn_options.map { |x| x.dump }.join(" ")}") }
+ _cset(:mvn_cmd_local) { command_local("#{mvn_bin_local.dump} #{mvn_options_local.map { |x| x.dump }.join(" ")}") }
_cset(:mvn_goals, %w(clean package))
_cset(:mvn_common_options) {
options = []
- options << "-P#{mvn_profiles.join(',')}" unless fetch(:mvn_profiles, []).empty?
+ options << "-P#{mvn_profiles.join(",")}" unless fetch(:mvn_profiles, []).empty?
options << "-Dmaven.test.skip=true" if fetch(:mvn_skip_tests, false)
options << "-U" if fetch(:mvn_update_snapshots, false)
options << "-B"
options
}
- _cset(:mvn_options) {
- options = mvn_common_options + fetch(:mvn_extra_options, [])
- if mvn_update_settings
- settings = File.join(mvn_settings_path, mvn_settings.first)
- options << "--settings=#{settings}"
- end
+ _cset(:mvn_default_options) {
+ options = mvn_common_options.dup
+ options += mvn_settings.map { |s| "--settings=#{File.join(mvn_settings_path, s).dump}" } if mvn_update_settings
options
}
- _cset(:mvn_options_local) {
- options = mvn_common_options + fetch(:mvn_extra_options_local, [])
- if mvn_update_settings_locally
- settings = File.join(mvn_settings_path_local, mvn_settings_local.first)
- options << "--settings=#{settings}"
- end
+ _cset(:mvn_default_options_local) {
+ options = mvn_common_options.dup
+ options += mvn_settings_local.map { |s| "--settings=#{File.join(mvn_settings_path_local, s).dump}" } if mvn_update_settings_locally
options
}
+ _cset(:mvn_options) { mvn_default_options + fetch(:mvn_extra_options, []) }
+ _cset(:mvn_options_local) { mvn_default_options_local + fetch(:mvn_extra_options_local, []) }
- desc("Setup maven.")
- task(:setup, :roles => :app, :except => { :no_release => true }) {
- transaction {
- install
- update_settings if mvn_update_settings
- setup_locally if mvn_compile_locally
- }
+ _cset(:mvn_setup_remotely) { mvn_update_remotely }
+ _cset(:mvn_setup_locally) { mvn_update_locally }
+ _cset(:mvn_update_remotely) { not(mvn_update_locally) }
+ _cset(:mvn_update_locally) { # perform update on localhost
+ if exists?(:mvn_compile_locally)
+ logger.info(":mvn_compile_locally has been deprecated. use :mvn_update_locally instead.")
+ fetch(:mvn_compile_locally, false)
+ else
+ false
+ end
}
- after 'deploy:setup', 'mvn:setup'
- desc("Setup maven locally.")
- task(:setup_locally, :except => { :no_release => true }) {
- transaction {
- install_locally
- update_settings_locally if mvn_update_settings_locally
+ if top.namespaces.key?(:multistage)
+ after "multistage:ensure", "mvn:setup_default_environment"
+ else
+ on :start do
+ if top.namespaces.key?(:multistage)
+ after "multistage:ensure", "mvn:setup_default_environment"
+ else
+ setup_default_environment
+ end
+ end
+ end
+
+ _cset(:mvn_environment_join_keys, %w(DYLD_LIBRARY_PATH LD_LIBRARY_PATH MANPATH PATH))
+ def _merge_environment(x, y)
+ x.merge(y) { |key, x_val, y_val|
+ if mvn_environment_join_keys.include?(key)
+ ( y_val.split(":") + x_val.split(":") ).uniq.join(":")
+ else
+ y_val
+ end
}
+ end
+
+ task(:setup_default_environment, :roles => mvn_roles, :except => { :no_release => true }) {
+ if fetch(:mvn_setup_default_environment, true)
+ set(:default_environment, _merge_environment(default_environment, mvn_environment))
+ end
}
- def _validate_archive(archive_file, checksum_file)
- if cmd = fetch(:mvn_checksum_cmd, nil)
- "test `#{cmd} #{archive_file} | cut -d' ' -f1` = `cat #{checksum_file}`"
+ def _invoke_command(cmdline, options={})
+ if options[:via] == :run_locally
+ run_locally(cmdline)
else
- "true"
+ invoke_command(cmdline, options)
end
end
- def _install(options={})
- path = options.delete(:path)
- bin = options.delete(:bin)
- checksum_file = options.delete(:checksum_file)
- checksum_url = options.delete(:checksum_url)
- archive_file = options.delete(:archive_file)
- archive_url = options.delete(:archive_url)
- dirs = [ File.dirname(checksum_file), File.dirname(archive_file), File.dirname(path) ].uniq()
+ def _download(uri, filename, options={})
+ options = fetch(:mvn_download_options, {}).merge(options)
+ if FileTest.exist?(filename)
+ logger.info("Found downloaded archive: #{filename}")
+ else
+ dirs = [ File.dirname(filename) ]
+ execute = []
+ execute << "mkdir -p #{dirs.uniq.map { |x| x.dump }.join(" ")}"
+ execute << "wget --no-verbose -O #{filename.dump} #{uri.dump}"
+ _invoke_command(execute.join(" && "), options)
+ end
+ end
+
+ def _upload(filename, remote_filename, options={})
+ _invoke_command("mkdir -p #{File.dirname(remote_filename).dump}", options)
+ transfer_if_modified(:up, filename, remote_filename, fetch(:mvn_upload_options, {}).merge(options))
+ end
+
+ def _install(filename, destination, options={})
execute = []
- execute << "mkdir -p #{dirs.join(' ')}"
- execute << (<<-EOS).gsub(/\s+/, ' ').strip
- if ! test -f #{archive_file}; then
- ( rm -f #{checksum_file}; wget --no-verbose -O #{checksum_file} #{checksum_url} ) &&
- wget --no-verbose -O #{archive_file} #{archive_url} &&
- #{_validate_archive(archive_file, checksum_file)} || ( rm -f #{archive_file}; false ) &&
- test -f #{archive_file};
- fi
- EOS
- execute << (<<-EOS).gsub(/\s+/, ' ').strip
- if ! test -x #{bin}; then
- ( test -d #{path} || tar xf #{archive_file} -C #{File.dirname(path)} ) &&
- test -x #{bin};
- fi
- EOS
- execute.join(' && ')
+ execute << "mkdir -p #{File.dirname(destination).dump}"
+ execute << "tar xf #{filename.dump} -C #{File.dirname(destination).dump}"
+ _invoke_command(execute.join(" && "), options)
end
- task(:install, :roles => :app, :except => { :no_release => true }) {
- run(_install(:path => mvn_path, :bin => mvn_bin,
- :checksum_file => mvn_checksum_file, :checksum_url => mvn_checksum_url,
- :archive_file => mvn_archive_file, :archive_url => mvn_archive_url))
- run("#{mvn_cmd} --version")
+ def _installed?(destination, options={})
+ mvn = File.join(destination, "bin", "mvn")
+ cmdline = "test -d #{destination.dump} && test -x #{mvn.dump}"
+ _invoke_command(cmdline, options)
+ true
+ rescue
+ false
+ end
+
+ ## setup
+ desc("Setup maven.")
+ task(:setup, :roles => mvn_roles, :except => { :no_release => true }) {
+ transaction {
+ setup_remotely if mvn_setup_remotely
+ setup_locally if mvn_setup_locally
+ }
}
+ after "deploy:setup", "mvn:setup"
- task(:install_locally, :except => { :no_release => true }) {
- run_locally(_install(:path => mvn_path_local, :bin => mvn_bin_local,
- :checksum_file => mvn_checksum_file_local, :checksum_url => mvn_checksum_url,
- :archive_file => mvn_archive_file_local, :archive_url => mvn_archive_url))
- run_locally("#{mvn_cmd_local} --version")
+ task(:setup_remotely, :roles => mvn_roles, :except => { :no_release => true }) {
+ _download(mvn_archive_url, mvn_archive_file_local, :via => :run_locally)
+ _upload(mvn_archive_file_local, mvn_archive_file)
+ unless _installed?(mvn_path)
+ _install(mvn_archive_file, mvn_path)
+ _installed?(mvn_path)
+ end
+ update_settings if mvn_update_settings
}
- task(:update_settings, :roles => :app, :except => { :no_release => true }) {
- mvn_settings.each do |f|
- safe_put(template(f, :path => mvn_template_path), File.join(mvn_settings_path, f))
+ desc("Setup maven locally.")
+ task(:setup_locally, :roles => mvn_roles, :except => { :no_release => true }) {
+ _download(mvn_archive_url, mvn_archive_file_local, :via => :run_locally)
+ unless _installed?(mvn_path_local, :via => :run_locally)
+ _install(mvn_archive_file_local, mvn_path_local, :via => :run_locally)
+ _installed?(mvn_path_local, :via => :run_locally)
end
- run("rm -f #{mvn_cleanup_settings.map { |x| x.dump }.join(' ')}") unless mvn_cleanup_settings.empty?
+ update_settings_locally if mvn_update_settings_locally
}
- task(:update_settings_locally, :except => { :no_release => true }) {
- mvn_settings_local.each do |f|
- File.write(File.join(mvn_settings_path_local, f), template(f, :path => mvn_template_path))
+ _cset(:mvn_update_settings) { mvn_setup_remotely and not(mvn_settings.empty?) }
+ _cset(:mvn_update_settings_locally) { mvn_setup_locally and not(mvn_settings_local.empty?) }
+ _cset(:mvn_settings_path) { mvn_tools_path }
+ _cset(:mvn_settings_path_local) { mvn_tools_path_local }
+ _cset(:mvn_settings, [])
+ _cset(:mvn_settings_local) { mvn_settings }
+ task(:update_settings, :roles => mvn_roles, :except => { :no_release => true }) {
+ mvn_settings.each do |file|
+ safe_put(template(file, :path => mvn_template_path), File.join(mvn_settings_path, file))
end
- run_locally("rm -f #{mvn_cleanup_settings_local.map { |x| x.dump }.join(' ')}") unless mvn_cleanup_settings_local.empty?
}
+ task(:update_settings_locally, :roles => mvn_roles, :except => { :no_release => true }) {
+ mvn_settings_local.each do |file|
+ destination = File.join(mvn_settings_path_local, file)
+ run_locally("mkdir -p #{File.dirname(destination).dump}")
+ File.write(destination, template(file, :path => mvn_template_path))
+ end
+ }
+
+ ## update
desc("Update maven build.")
- task(:update, :roles => :app, :except => { :no_release => true }) {
+ task(:update, :roles => mvn_roles, :except => { :no_release => true }) {
transaction {
- if mvn_compile_locally
- update_locally
- else
- execute
- end
+ update_remotely if mvn_update_remotely
+ update_locally if mvn_update_locally
}
}
- after 'deploy:finalize_update', 'mvn:update'
+ _cset(:mvn_update_hook_type, :after)
+ _cset(:mvn_update_hook, "deploy:finalize_update")
+ on(:start) do
+ [ mvn_update_hook ].flatten.each do |hook|
+ send(mvn_update_hook_type, hook, "mvn:update") if hook
+ end
+ end
+ task(:update_remotely, :roles => mvn_roles, :except => { :no_release => true }) {
+ execute_remotely
+ }
+
desc("Update maven build locally.")
- task(:update_locally, :except => { :no_release => true }) {
- transaction {
- execute_locally
- upload_locally
- }
+ task(:update_locally, :roles => mvn_roles, :except => { :no_release => true }) {
+ execute_locally
+ upload_locally
}
- def _mvn(cmd, path, goals=[])
- "cd #{path.dump} && #{cmd} #{goals.map { |s| s.dump }.join(' ')}"
- end
-
- def _mvn_parse_version(s)
+ def _parse_project_version(s)
# FIXME: is there any better way to get project version?
s.split(/(?:\r?\n)+/).reject { |line| /^\[[A-Z]+\]/ =~ line }.last
end
- _cset(:mvn_release_build, false)
- _cset(:mvn_snapshot_pattern, /-SNAPSHOT$/i)
_cset(:mvn_project_version) {
- _mvn_parse_version(capture(_mvn(mvn_cmd, mvn_project_path, %w(-Dexpression=project.version help:evaluate))))
+ _parse_project_version(mvn.exec(%w(-Dexpression=project.version help:evaluate), :via => :capture))
}
_cset(:mvn_project_version_local) {
- _mvn_parse_version(run_locally(_mvn(mvn_cmd_local, mvn_project_path_local, %w(-Dexpression=project.version help:evaluate))))
+ _parse_project_version(mvn.exec_locally(%w(-Dexpression=project.version help:evaluate), :via => :capture_locally))
}
-
- def _validate_project_version(version_key)
- if mvn_release_build
- version = fetch(version_key)
- if mvn_snapshot_pattern === version
- abort("Skip to build project since \`#{version}' is a SNAPSHOT version.")
- end
+ _cset(:mvn_snapshot_pattern, /-SNAPSHOT$/i)
+ def _validate_project_version(key)
+ if fetch(:mvn_release_build, false)
+ version = fetch(key)
+ abort("Skip to build project since \`#{version}' is a SNAPSHOT version.") if mvn_snapshot_pattern === version
end
end
desc("Perform maven build.")
- task(:execute, :roles => :app, :except => { :no_release => true }) {
- on_rollback {
- run(_mvn(mvn_cmd, mvn_project_path, %w(clean)))
- }
+ task(:execute, :roles => mvn_roles, :except => { :no_release => true }) {
+ execute_remotely
+ }
+ task(:execute_remotely, :roles => mvn_roles, :except => { :no_release => true }) {
+ on_rollback do
+ mvn.exec("clean")
+ end
_validate_project_version(:mvn_project_version)
- run(_mvn(mvn_cmd, mvn_project_path, mvn_goals))
+ mvn.exec(mvn_goals)
}
desc("Perform maven build locally.")
- task(:execute_locally, :roles => :app, :except => { :no_release => true }) {
- on_rollback {
- run_locally(_mvn(mvn_cmd_local, mvn_project_path_local, %w(clean)))
- }
+ task(:execute_locally, :roles => mvn_roles, :except => { :no_release => true }) {
+ on_rollback do
+ mvn.exec_locally("clean")
+ end
_validate_project_version(:mvn_project_version_local)
- cmdline = _mvn(mvn_cmd_local, mvn_project_path_local, mvn_goals)
- logger.info(cmdline)
- abort("execution failure") unless system(cmdline)
+ mvn.exec_locally(mvn_goals)
}
- _cset(:mvn_tar, 'tar')
- _cset(:mvn_tar_local, 'tar')
- _cset(:mvn_target_archive) {
- "#{mvn_target_path}.tar.gz"
- }
- _cset(:mvn_target_archive_local) {
- "#{mvn_target_path_local}.tar.gz"
- }
- task(:upload_locally, :roles => :app, :except => { :no_release => true }) {
- on_rollback {
- run("rm -rf #{mvn_target_path} #{mvn_target_archive}")
- }
+ _cset(:mvn_target_path) { File.join(mvn_project_path, "target") }
+ _cset(:mvn_target_path_local) { File.join(mvn_project_path_local, "target") }
+ task(:upload_locally, :roles => mvn_roles, :except => { :no_release => true }) {
+ on_rollback do
+ run("rm -rf #{mvn_target_path.dump}")
+ end
+ filename = "#{mvn_target_path_local}.tar.gz"
+ remote_filename = "#{mvn_target_path}.tar.gz"
begin
- run_locally("cd #{File.dirname(mvn_target_path_local)} && #{mvn_tar_local} chzf #{mvn_target_archive_local} #{File.basename(mvn_target_path_local)}")
- upload(mvn_target_archive_local, mvn_target_archive)
- run("cd #{File.dirname(mvn_target_path)} && #{mvn_tar} xzf #{mvn_target_archive} && rm -f #{mvn_target_archive}")
+ run_locally("cd #{File.dirname(mvn_target_path_local).dump} && tar chzf #{filename.dump} #{File.basename(mvn_target_path_local).dump}")
+ run("mkdir -p #{File.dirname(mvn_target_path).dump}")
+ top.upload(filename, remote_filename)
+ run("cd #{File.dirname(mvn_target_path).dump} && tar xzf #{remote_filename.dump}")
ensure
- run_locally("rm -f #{mvn_target_archive_local}")
+ run("rm -f #{remote_filename.dump}") rescue nil
+ run_locally("rm -f #{filename.dump}") rescue nil
end
}
+
+ def _exec_command(args=[], options={})
+ args = [ args ].flatten
+ mvn = options.fetch(:mvn, "mvn")
+ execute = []
+ execute << "cd #{options[:path].dump}" if options.key?(:path)
+ execute << "#{mvn} #{args.map { |x| x.dump }.join(" ")}"
+ execute.join(" && ")
+ end
+
+ ## public methods
+ def exec(args=[], options={})
+ cmdline = _exec_command(args, { :path => mvn_project_path, :mvn => mvn_cmd, :via => :run }.merge(options))
+ _invoke_command(cmdline, options)
+ end
+
+ def exec_locally(args=[], options={})
+ via = options.delete(:via)
+ cmdline = _exec_command(args, { :path => mvn_project_path_local, :mvn => mvn_cmd_local, :via => :run_locally }.merge(options))
+ if via == :capture_locally
+ _invoke_command(cmdline, options.merge(:via => :run_locally))
+ else
+ logger.trace("executing locally: #{cmdline.dump}")
+ elapsed = Benchmark.realtime do
+ system(cmdline)
+ end
+ if $?.to_i > 0 # $? is command exit code (posix style)
+ raise Capistrano::LocalArgumentError, "Command #{cmd} returned status code #{$?}"
+ end
+ logger.trace "command finished in #{(elapsed * 1000).round}ms"
+ end
+ end
}
}
end
end
end