blob: 56365f60326de4e01ea3eaa93c99d5746e4fc655 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with this
# work for additional information regarding copyright ownership. The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# Portion of this file derived from Rake.
# Copyright (c) 2003, 2004 Jim Weirich
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
require 'benchmark'
require 'rake'
require 'rubygems/source_info_cache'
require 'buildr/core/application_cli'
require 'buildr/core/util'
# Gem::user_home is nice, but ENV['HOME'] lets you override from the environment.
ENV["HOME"] ||= File.expand_path(Gem::user_home)
ENV['BUILDR_ENV'] ||= 'development'
module Buildr
# Provide settings that come from three sources.
#
# User settings are placed in the .buildr/settings.yaml file located in the user's home directory.
# The should only be used for settings that are specific to the user and applied the same way
# across all builds. Example for user settings are preferred repositories, path to local repository,
# user/name password for uploading to remote repository.
#
# Build settings are placed in the build.yaml file located in the build directory. They help keep
# the buildfile and build.yaml file simple and readable, working to the advantages of each one.
# Example for build settings are gems, repositories and artifacts used by that build.
#
# Profile settings are placed in the profiles.yaml file located in the build directory. They provide
# settings that differ in each environment the build runs in. For example, URLs and database
# connections will be different when used in development, test and production environments.
# The settings for the current environment are obtained by calling #profile.
class Settings
def initialize(application) #:nodoc:
@application = application
@user = load_from('settings', @application.home_dir)
@build = load_from('build')
@profiles = load_from('profiles')
end
# User settings loaded from setting.yaml file in user's home directory.
attr_reader :user
# Build settings loaded from build.yaml file in build directory.
attr_reader :build
# Profiles loaded from profiles.yaml file in build directory.
attr_reader :profiles
# :call-seq:
# profile => hash
#
# Returns the profile for the current environment.
def profile
profiles[@application.environment] ||= {}
end
private
def load_from(base_name, dir = nil)
file_name = ['yaml', 'yml'].map { |ext| File.expand_path("#{base_name}.#{ext}", dir) }.find { |fn| File.exist?(fn) }
return {} unless file_name
yaml = YAML.load(File.read(file_name)) || {}
fail "Expecting #{file_name} to be a map (name: value)!" unless Hash === yaml
@application.build_files << file_name
yaml
end
end
class Application < Rake::Application #:nodoc:
DEFAULT_BUILDFILES = ['buildfile', 'Buildfile'] + DEFAULT_RAKEFILES
include CommandLineInterface
attr_reader :rakefiles, :requires
private :rakefiles, :requires
def initialize
super
@rakefiles = DEFAULT_BUILDFILES
@name = 'Buildr'
@requires = []
@top_level_tasks = []
parse_options
collect_tasks
top_level_tasks.unshift 'buildr:initialize'
@home_dir = File.expand_path('.buildr', ENV['HOME'])
mkpath @home_dir unless File.exist?(@home_dir)
@environment = ENV['BUILDR_ENV'] ||= 'development'
end
# Returns list of Gems associated with this buildfile, as listed in build.yaml.
# Each entry is of type Gem::Specification.
attr_reader :gems
# Buildr home directory, .buildr under user's home directory.
attr_reader :home_dir
# Copied from BUILD_ENV.
attr_reader :environment
# Returns the Settings associated with this build.
def settings
@settings ||= Settings.new(self)
end
# :call-seq:
# buildfile
def buildfile
rakefile
end
# :call-seq:
# build_files => files
#
# Returns a list of build files. These are files used by the build,
def build_files
[buildfile].compact + Array(@build_files)
end
# Returns Gem::Specification for every listed and installed Gem, Gem::Dependency
# for listed and uninstalled Gem, which is the installed before loading the buildfile.
def listed_gems #:nodoc:
Array(settings.build['gems']).map do |dep|
name, trail = dep.scan(/^\s*(\S*)\s*(.*)\s*$/).first
versions = trail.scan(/[=><~!]{0,2}\s*[\d\.]+/)
versions = ['>= 0'] if versions.empty?
dep = Gem::Dependency.new(name, versions)
Gem::SourceIndex.from_installed_gems.search(dep).last || dep
end
end
private :listed_gems
def run
times = Benchmark.measure do
standard_exception_handling do
find_buildfile
load_gems
load_artifacts
load_buildfile
top_level
load_tasks
end
end
if verbose
real = []
real << ("%ih" % (times.real / 3600)) if times.real >= 3600
real << ("%im" % ((times.real / 60) % 60)) if times.real >= 60
real << ("%.3fs" % (times.real % 60))
puts "Completed in #{real.join}"
end
end
# Load artifact specs from the build.yaml file, making them available
# by name ( ruby symbols ).
def load_artifacts #:nodoc:
hash = settings.build['artifacts']
return unless hash
raise "Expected 'artifacts' element to be a hash" unless Hash === hash
# Currently we only use one artifact namespace to rule them all. (the root NS)
Buildr::ArtifactNamespace.load(:root => hash)
end
# Load/install all Gems specified in build.yaml file.
def load_gems #:nodoc:
missing_deps, installed = listed_gems.partition { |gem| gem.is_a?(Gem::Dependency) }
unless missing_deps.empty?
remote = missing_deps.map { |dep| Gem::SourceInfoCache.search(dep).last || dep }
not_found_deps, install = remote.partition { |gem| gem.is_a?(Gem::Dependency) }
fail Gem::LoadError, "Build requires the gems #{not_found_deps.join(', ')}, which cannot be found in local or remote repository." unless not_found_deps.empty?
uses = "This build requires the gems #{install.map(&:full_name).join(', ')}:"
fail Gem::LoadError, "#{uses} to install, run Buildr interactively." unless $stdout.isatty
unless agree("#{uses} do you want me to install them? [Y/n]", true)
fail Gem::LoadError, 'Cannot build without these gems.'
end
install.each do |spec|
say "Installing #{spec.full_name} ... " if verbose
Util.ruby 'install', spec.name, '-v', spec.version.to_s, :command => 'gem', :sudo => true, :verbose => false
Gem.source_index.load_gems_in Gem::SourceIndex.installed_spec_directories
end
installed += install
end
installed.each do |spec|
if gem(spec.name, spec.version.to_s)
# FileList[spec.require_paths.map { |path| File.expand_path("#{path}/*.rb", spec.full_gem_path) }].
# map { |path| File.basename(path) }.each { |file| require file }
# FileList[File.expand_path('tasks/*.rake', spec.full_gem_path)].each do |file|
# Buildr.application.add_import file
# end
end
end
@gems = installed
end
def find_buildfile
here = Dir.pwd
while ! have_rakefile
Dir.chdir('..')
if Dir.pwd == here || options.nosearch
error = "No Buildfile found (looking for: #{@rakefiles.join(', ')})"
if STDIN.isatty
chdir(original_dir) { task('generate').invoke }
exit 1
else
raise error
end
end
here = Dir.pwd
end
end
def load_buildfile
@requires.each { |name| require name }
puts "(in #{Dir.pwd}, #{environment})"
load File.expand_path(@rakefile) if @rakefile != ''
load_imports
end
# Loads buildr.rake files from users home directory and project directory.
# Loads custom tasks from .rake files in tasks directory.
def load_tasks #:nodoc:
@build_files = [ File.expand_path('buildr.rb', ENV['HOME']), 'buildr.rb' ].select { |file| File.exist?(file) }
@build_files += [ File.expand_path('buildr.rake', ENV['HOME']), File.expand_path('buildr.rake') ].
select { |file| File.exist?(file) }.each { |file| warn "Please use '#{file.ext('rb')}' instead of '#{file}'" }
#Load local tasks that can be used in the Buildfile.
@build_files += Dir["#{Dir.pwd}/tasks/*.rake"]
@build_files.each do |file|
unless $LOADED_FEATURES.include?(file)
load file
$LOADED_FEATURES << file
end
end
true
end
private :load_tasks
# :call-seq:
# deprecated(message)
#
# Use with deprecated methods and classes. This method automatically adds the file name and line number,
# and the text 'Deprecated' before the message, and eliminated duplicate warnings. It only warns when
# running in verbose mode.
#
# For example:
# deprecated 'Please use new_foo instead of foo.'
def deprecated(message) #:nodoc:
return unless verbose
"#{caller[1]}: Deprecated: #{message}".tap do |message|
@deprecated ||= {}
unless @deprecated[message]
@deprecated[message] = true
warn message
end
end
end
# Not for external consumption.
def switch_to_namespace(names) #:nodoc:
current, @scope = @scope, names
begin
yield
ensure
@scope = current
end
end
end
class << self
task 'buildr:initialize' do
Buildr.load_tasks_and_local_files
end
# Returns the Buildr::Application object.
def application
Rake.application
end
def application=(app) #:nodoc:
Rake.application = app
end
# Returns the Settings associated with this build.
def settings
Buildr.application.settings
end
# Copied from BUILD_ENV.
def environment
Buildr.application.environment
end
end
Buildr.application = Buildr::Application.new
end
# Add a touch of colors (red) to warnings.
if $stdout.isatty
begin
require 'Win32/Console/ANSI' if Config::CONFIG['host_os'] =~ /mswin/
HighLine.use_color = true
rescue LoadError
end
end
if HighLine.use_color?
module Kernel #:nodoc:
alias :warn_without_color :warn
def warn(message)
warn_without_color $terminal.color(message.to_s, :red)
end
end
end
module Rake #:nodoc
class Task #:nodoc:
def invoke(*args)
task_args = TaskArguments.new(arg_names, args)
invoke_with_call_chain(task_args, Thread.current[:rake_chain] || InvocationChain::EMPTY)
end
def invoke_with_call_chain(task_args, invocation_chain)
new_chain = InvocationChain.append(self, invocation_chain)
@lock.synchronize do
if application.options.trace
puts "** Invoke #{name} #{format_trace_flags}"
end
return if @already_invoked
@already_invoked = true
invoke_prerequisites(task_args, new_chain)
begin
old_chain, Thread.current[:rake_chain] = Thread.current[:rake_chain], new_chain
execute(task_args) if needed?
ensure
Thread.current[:rake_chain] = nil
end
end
end
end
end