Skip to content

Instantly share code, notes, and snippets.

@cilquirm
Created October 15, 2008 02:38
Show Gist options
  • Save cilquirm/16840 to your computer and use it in GitHub Desktop.
Save cilquirm/16840 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
require 'rubygems'
require 'thor'
require 'fileutils'
require 'yaml'
# Important - don't change this line or its position
MERB_THOR_VERSION = '0.0.5'
##############################################################################
module ColorfulMessages
# red
def error(*messages)
puts messages.map { |msg| "\033[1;31m#{msg}\033[0m" }
end
# yellow
def warning(*messages)
puts messages.map { |msg| "\033[1;33m#{msg}\033[0m" }
end
# green
def success(*messages)
puts messages.map { |msg| "\033[1;32m#{msg}\033[0m" }
end
alias_method :message, :success
# magenta
def note(*messages)
puts messages.map { |msg| "\033[1;35m#{msg}\033[0m" }
end
# blue
def info(*messages)
puts messages.map { |msg| "\033[1;34m#{msg}\033[0m" }
end
end
##############################################################################
require 'rubygems/dependency_installer'
require 'rubygems/uninstaller'
require 'rubygems/dependency'
module GemManagement
include ColorfulMessages
# Install a gem - looks remotely and local gem cache;
# won't process rdoc or ri options.
def install_gem(gem, options = {})
refresh = options.delete(:refresh) || []
from_cache = (options.key?(:cache) && options.delete(:cache))
if from_cache
install_gem_from_cache(gem, options)
else
version = options.delete(:version)
Gem.configuration.update_sources = false
# Limit source index to install dir
update_source_index(options[:install_dir]) if options[:install_dir]
installer = Gem::DependencyInstaller.new(options.merge(:user_install => false))
# Force-refresh certain gems by excluding them from the current index
if !options[:ignore_dependencies] && refresh.respond_to?(:include?) && !refresh.empty?
source_index = installer.instance_variable_get(:@source_index)
source_index.gems.each do |name, spec|
source_index.gems.delete(name) if refresh.include?(spec.name)
end
end
exception = nil
begin
installer.install gem, version
rescue Gem::InstallError => e
exception = e
rescue Gem::GemNotFoundException => e
if from_cache && gem_file = find_gem_in_cache(gem, version)
puts "Located #{gem} in gem cache..."
installer.install gem_file
else
exception = e
end
rescue => e
exception = e
end
if installer.installed_gems.empty? && exception
error "Failed to install gem '#{gem} (#{version || 'any version'})' (#{exception.message})"
end
installer.installed_gems.each do |spec|
success "Successfully installed #{spec.full_name}"
end
return !installer.installed_gems.empty?
end
end
# Install a gem - looks in the system's gem cache instead of remotely;
# won't process rdoc or ri options.
def install_gem_from_cache(gem, options = {})
version = options.delete(:version)
Gem.configuration.update_sources = false
installer = Gem::DependencyInstaller.new(options.merge(:user_install => false))
exception = nil
begin
if gem_file = find_gem_in_cache(gem, version)
puts "Located #{gem} in gem cache..."
installer.install gem_file
else
raise Gem::InstallError, "Unknown gem #{gem}"
end
rescue Gem::InstallError => e
exception = e
end
if installer.installed_gems.empty? && exception
error "Failed to install gem '#{gem}' (#{e.message})"
end
installer.installed_gems.each do |spec|
success "Successfully installed #{spec.full_name}"
end
end
# Install a gem from source - builds and packages it first then installs.
#
# Examples:
# install_gem_from_source(source_dir, :install_dir => ...)
# install_gem_from_source(source_dir, gem_name)
# install_gem_from_source(source_dir, :skip => [...])
def install_gem_from_source(source_dir, *args)
installed_gems = []
Dir.chdir(source_dir) do
opts = args.last.is_a?(Hash) ? args.pop : {}
gem_name = args[0] || File.basename(source_dir)
gem_pkg_dir = File.join(source_dir, 'pkg')
gem_pkg_glob = File.join(gem_pkg_dir, "#{gem_name}-*.gem")
skip_gems = opts.delete(:skip) || []
# Cleanup what's already there
clobber(source_dir)
FileUtils.mkdir_p(gem_pkg_dir) unless File.directory?(gem_pkg_dir)
# Recursively process all gem packages within the source dir
skip_gems << gem_name
packages = package_all(source_dir, skip_gems)
if packages.length == 1
# The are no subpackages for the main package
refresh = [gem_name]
else
# Gather all packages into the top-level pkg directory
packages.each do |pkg|
FileUtils.copy_entry(pkg, File.join(gem_pkg_dir, File.basename(pkg)))
end
# Finally package the main gem - without clobbering the already copied pkgs
package(source_dir, false)
# Gather subgems to refresh during installation of the main gem
refresh = packages.map do |pkg|
File.basename(pkg, '.gem')[/^(.*?)-([\d\.]+)$/, 1] rescue nil
end.compact
# Install subgems explicitly even if ignore_dependencies is set
if opts[:ignore_dependencies]
refresh.each do |name|
gem_pkg = Dir[File.join(gem_pkg_dir, "#{name}-*.gem")][0]
install_pkg(gem_pkg, opts)
end
end
end
# Finally install the main gem
if install_pkg(Dir[gem_pkg_glob][0], opts.merge(:refresh => refresh))
installed_gems = refresh
else
installed_gems = []
end
end
installed_gems
end
def install_pkg(gem_pkg, opts = {})
if (gem_pkg && File.exists?(gem_pkg))
# Needs to be executed from the directory that contains all packages
Dir.chdir(File.dirname(gem_pkg)) { install_gem(gem_pkg, opts) }
else
false
end
end
# Uninstall a gem.
def uninstall_gem(gem, options = {})
if options[:version] && !options[:version].is_a?(Gem::Requirement)
options[:version] = Gem::Requirement.new ["= #{options[:version]}"]
end
update_source_index(options[:install_dir]) if options[:install_dir]
Gem::Uninstaller.new(gem, options).uninstall
end
def clobber(source_dir)
Dir.chdir(source_dir) do
system "#{Gem.ruby} -S rake -s clobber" unless File.exists?('Thorfile')
end
end
def package(source_dir, clobber = true)
Dir.chdir(source_dir) do
if File.exists?('Thorfile')
thor ":package"
elsif File.exists?('Rakefile')
rake "clobber" if clobber
rake "package"
end
end
Dir[File.join(source_dir, 'pkg/*.gem')]
end
def package_all(source_dir, skip = [], packages = [])
if Dir[File.join(source_dir, '{Rakefile,Thorfile}')][0]
name = File.basename(source_dir)
Dir[File.join(source_dir, '*', '{Rakefile,Thorfile}')].each do |taskfile|
package_all(File.dirname(taskfile), skip, packages)
end
packages.push(*package(source_dir)) unless skip.include?(name)
end
packages.uniq
end
def rake(cmd)
cmd << " >/dev/null" if $SILENT && !Gem.win_platform?
system "#{Gem.ruby} -S #{which('rake')} -s #{cmd} >/dev/null"
end
def thor(cmd)
cmd << " >/dev/null" if $SILENT && !Gem.win_platform?
system "#{Gem.ruby} -S #{which('thor')} #{cmd}"
end
# Use the local bin/* executables if available.
def which(executable)
if File.executable?(exec = File.join(Dir.pwd, 'bin', executable))
exec
else
executable
end
end
# Partition gems into system, local and missing gems
def partition_dependencies(dependencies, gem_dir)
system_specs, local_specs, missing_deps = [], [], []
if gem_dir && File.directory?(gem_dir)
gem_dir = File.expand_path(gem_dir)
::Gem.clear_paths; ::Gem.path.unshift(gem_dir)
::Gem.source_index.refresh!
dependencies.each do |dep|
gemspecs = ::Gem.source_index.search(dep)
local = gemspecs.reverse.find { |s| s.loaded_from.index(gem_dir) == 0 }
if local
local_specs << local
elsif gemspecs.last
system_specs << gemspecs.last
else
missing_deps << dep
end
end
::Gem.clear_paths
end
[system_specs, local_specs, missing_deps]
end
# Create a modified executable wrapper in the specified bin directory.
def ensure_bin_wrapper_for(gem_dir, bin_dir, *gems)
options = gems.last.is_a?(Hash) ? gems.last : {}
options[:no_minigems] ||= []
if bin_dir && File.directory?(bin_dir)
gems.each do |gem|
if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{gem}-*.gemspec")].last
spec = Gem::Specification.load(gemspec_path)
enable_minigems = !options[:no_minigems].include?(spec.name)
spec.executables.each do |exec|
executable = File.join(bin_dir, exec)
message "Writing executable wrapper #{executable}"
File.open(executable, 'w', 0755) do |f|
f.write(executable_wrapper(spec, exec, enable_minigems))
end
end
end
end
end
end
private
def executable_wrapper(spec, bin_file_name, minigems = true)
requirements = ['minigems', 'rubygems']
requirements.reverse! unless minigems
try_req, then_req = requirements
<<-TEXT
#!/usr/bin/env ruby
#
# This file was generated by Merb's GemManagement
#
# The application '#{spec.name}' is installed as part of a gem, and
# this file is here to facilitate running it.
begin
require '#{try_req}'
rescue LoadError
require '#{then_req}'
end
if File.directory?(gems_dir = File.join(Dir.pwd, 'gems')) ||
File.directory?(gems_dir = File.join(File.dirname(__FILE__), '..', 'gems'))
$BUNDLE = true; Gem.clear_paths; Gem.path.unshift(gems_dir)
end
version = "#{Gem::Requirement.default}"
if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
version = $1
ARGV.shift
end
gem '#{spec.name}', version
load '#{bin_file_name}'
TEXT
end
def find_gem_in_cache(gem, version)
spec = if version
version = Gem::Requirement.new ["= #{version}"] unless version.is_a?(Gem::Requirement)
Gem.source_index.find_name(gem, version).first
else
Gem.source_index.find_name(gem).sort_by { |g| g.version }.last
end
if spec && File.exists?(gem_file = "#{spec.installation_path}/cache/#{spec.full_name}.gem")
gem_file
end
end
def update_source_index(dir)
Gem.source_index.load_gems_in(File.join(dir, 'specifications'))
end
end
##############################################################################
class SourceManager
include ColorfulMessages
attr_accessor :source_dir
def initialize(source_dir)
self.source_dir = source_dir
end
def clone(name, url)
FileUtils.cd(source_dir) do
raise "destination directory already exists" if File.directory?(name)
system("git clone --depth 1 #{url} #{name}")
end
rescue => e
error "Unable to clone #{name} repository (#{e.message})"
end
def update(name, url)
if File.directory?(repository_dir = File.join(source_dir, name))
FileUtils.cd(repository_dir) do
repos = existing_repos(name)
fork_name = url[/.com\/+?(.+)\/.+\.git/u, 1]
if url == repos["origin"]
# Pull from the original repository - no branching needed
info "Pulling from origin: #{url}"
system "git fetch; git checkout master; git rebase origin/master"
elsif repos.values.include?(url) && fork_name
# Update and switch to a remote branch for a particular github fork
info "Switching to remote branch: #{fork_name}"
system "git checkout -b #{fork_name} #{fork_name}/master"
system "git rebase #{fork_name}/master"
elsif fork_name
# Create a new remote branch for a particular github fork
info "Adding a new remote branch: #{fork_name}"
system "git remote add -f #{fork_name} #{url}"
system "git checkout -b #{fork_name} #{fork_name}/master"
else
warning "No valid repository found for: #{name}"
end
end
return true
else
warning "No valid repository found at: #{repository_dir}"
end
rescue => e
error "Unable to update #{name} repository (#{e.message})"
return false
end
def existing_repos(name)
repos = []
FileUtils.cd(File.join(source_dir, name)) do
repos = %x[git remote -v].split("\n").map { |branch| branch.split(/\s+/) }
end
Hash[*repos.flatten]
end
end
##############################################################################
module MerbThorHelper
attr_accessor :include_dependencies
def self.included(base)
base.send(:include, ColorfulMessages)
base.extend ColorfulMessages
end
def install_dependency(dependency, opts = {})
opts[:version] ||= dependency.version_requirements.to_s
Merb::Gem.install(dependency.name, default_install_options.merge(opts))
end
def source_manager
@_source_manager ||= SourceManager.new(source_dir)
end
def extract_repositories(names)
repos = []
names.each do |name|
if repo_url = Merb::Source.repo(name, options[:sources])
# A repository entry for this dependency exists
repo = [name, repo_url]
repos << repo unless repos.include?(repo)
elsif (repo_name = Merb::Stack.lookup_repository_name(name)) &&
(repo_url = Merb::Source.repo(repo_name, options[:sources]))
# A parent repository entry for this dependency exists
repo = [repo_name, repo_url]
unless repos.include?(repo)
puts "Found #{repo_name}/#{name} at #{repo_url}"
repos << repo
end
end
end
repos
end
def update_dependency_repositories(dependencies)
repos = extract_repositories(dependencies.map { |d| d.name })
update_repositories(repos)
end
def update_repositories(repos)
repos.each do |(name, url)|
if File.directory?(repository_dir = File.join(source_dir, name))
message "Updating or branching #{name}..."
source_manager.update(name, url)
else
message "Cloning #{name} repository from #{url}..."
source_manager.clone(name, url)
end
end
end
def install_dependency_from_source(dependency, opts = {})
matches = Dir[File.join(source_dir, "**", dependency.name, "{Rakefile,Thorfile}")]
matches.reject! { |m| File.basename(m) == 'Thorfile' }
if matches.length == 1 && matches[0]
if File.directory?(gem_src_dir = File.dirname(matches[0]))
begin
Merb::Gem.install_gem_from_source(gem_src_dir, default_install_options.merge(opts))
puts "Installed #{dependency.name}"
return true
rescue => e
warning "Unable to install #{dependency.name} from source (#{e.message})"
end
else
msg = "Unknown directory: #{gem_src_dir}"
warning "Unable to install #{dependency.name} from source (#{msg})"
end
elsif matches.length > 1
error "Ambigous source(s) for dependency: #{dependency.name}"
matches.each { |m| puts "- #{m}" }
end
return false
end
def clobber_dependencies!
if options[:force] && gem_dir && File.directory?(gem_dir)
# Remove all existing local gems by clearing the gems directory
if dry_run?
note 'Clearing existing local gems...'
else
message 'Clearing existing local gems...'
FileUtils.rm_rf(gem_dir) && FileUtils.mkdir_p(default_gem_dir)
end
elsif !local.empty?
# Uninstall all local versions of the gems to install
if dry_run?
note 'Uninstalling existing local gems:'
local.each { |gemspec| note "Uninstalled #{gemspec.name}" }
else
message 'Uninstalling existing local gems:'
local.each do |gemspec|
Merb::Gem.uninstall(gemspec.name, default_uninstall_options)
end
end
end
end
def display_gemspecs(gemspecs)
if gemspecs.empty?
puts "- none"
else
gemspecs.each do |spec|
if hint = Dir[File.join(spec.full_gem_path, '*.strategy')][0]
strategy = File.basename(hint, '.strategy')
puts "- #{spec.full_name} (#{strategy})"
else
puts "~ #{spec.full_name}" # unknown strategy
end
end
end
end
def display_dependencies(dependencies)
if dependencies.empty?
puts "- none"
else
dependencies.each { |d| puts "- #{d.name} (#{d.version_requirements})" }
end
end
def default_install_options
{ :install_dir => gem_dir, :ignore_dependencies => ignore_dependencies? }
end
def default_uninstall_options
{ :install_dir => gem_dir, :ignore => true, :all => true, :executables => true }
end
def dry_run?
options[:"dry-run"]
end
def ignore_dependencies?
options[:"ignore-dependencies"] || !include_dependencies?
end
def include_dependencies?
options[:"include-dependencies"] || self.include_dependencies
end
# The current working directory, or Merb app root (--merb-root option).
def working_dir
@_working_dir ||= File.expand_path(options['merb-root'] || Dir.pwd)
end
# We should have a ./src dir for local and system-wide management.
def source_dir
@_source_dir ||= File.join(working_dir, 'src')
create_if_missing(@_source_dir)
@_source_dir
end
# If a local ./gems dir is found, return it.
def gem_dir
if File.directory?(dir = default_gem_dir)
dir
end
end
def default_gem_dir
File.join(working_dir, 'gems')
end
# If we're in a Merb app, we can have a ./bin directory;
# create it if it's not there.
def bin_dir
@_bin_dir ||= begin
if gem_dir
dir = File.join(working_dir, 'bin')
create_if_missing(dir)
dir
end
end
end
# Helper to create dir unless it exists.
def create_if_missing(path)
FileUtils.mkdir(path) unless File.exists?(path)
end
def ensure_bin_wrapper_for(*gems)
Merb::Gem.ensure_bin_wrapper_for(gem_dir, bin_dir, *gems)
end
def sudo
ENV['THOR_SUDO'] ||= "sudo"
sudo = Gem.win_platform? ? "" : ENV['THOR_SUDO']
end
def local_gemspecs(directory = gem_dir)
if File.directory?(specs_dir = File.join(directory, 'specifications'))
Dir[File.join(specs_dir, '*.gemspec')].map do |gemspec_path|
gemspec = Gem::Specification.load(gemspec_path)
gemspec.loaded_from = gemspec_path
gemspec
end
else
[]
end
end
end
##############################################################################
$SILENT = true # don't output all the mess some rake package tasks spit out
module Merb
class Dependencies < Thor
# The Dependencies tasks will install dependencies based on actual application
# dependencies. For this, the application is queried for any dependencies.
# All operations will be performed within this context.
attr_accessor :system, :local, :missing
include MerbThorHelper
global_method_options = {
"--merb-root" => :optional, # the directory to operate on
"--include-dependencies" => :boolean, # gather sub-dependencies
"--stack" => :boolean, # gather only stack dependencies
"--no-stack" => :boolean, # gather only non-stack dependencies
"--config" => :boolean, # gather dependencies from yaml config
"--config-file" => :optional, # gather from the specified yaml config
"--version" => :optional # gather specific version of framework
}
method_options global_method_options
def initialize(*args); super; end
# List application dependencies.
#
# By default all dependencies are listed, partitioned into system, local and
# currently missing dependencies. The first argument allows you to filter
# on any of the partitionings. A second argument can be used to filter on
# a set of known components, like all merb-more gems for example.
#
# Examples:
#
# merb:dependencies:list # list all dependencies - the default
# merb:dependencies:list local # list only local gems
# merb:dependencies:list all merb-more # list only merb-more related dependencies
# merb:dependencies:list --stack # list framework dependencies
# merb:dependencies:list --no-stack # list 3rd party dependencies
# merb:dependencies:list --config # list dependencies from the default config
# merb:dependencies:list --config-file file.yml # list from the specified config file
desc 'list [all|local|system|missing] [comp]', 'Show application dependencies'
def list(filter = 'all', comp = nil)
deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies
self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir)
case filter
when 'all'
message 'Installed system gem dependencies:'
display_gemspecs(system)
message 'Installed local gem dependencies:'
display_gemspecs(local)
unless missing.empty?
error 'Missing gem dependencies:'
display_dependencies(missing)
end
when 'system'
message 'Installed system gem dependencies:'
display_gemspecs(system)
when 'local'
message 'Installed local gem dependencies:'
display_gemspecs(local)
when 'missing'
error 'Missing gem dependencies:'
display_dependencies(missing)
else
warning "Invalid listing filter '#{filter}'"
end
if missing.size > 0
info "Some dependencies are currently missing!"
elsif local.size == deps.size
info "All dependencies have been bundled with the application."
elsif local.size > system.size
info "Most dependencies have been bundled with the application."
elsif system.size > 0 && local.size > 0
info "Some dependencies have been bundled with the application."
elsif local.empty? && system.size == deps.size
info "All dependencies are available on the system."
end
end
# Install application dependencies.
#
# By default all required dependencies are installed. The first argument
# specifies which strategy to use: stable or edge. A second argument can be
# used to filter on a set of known components.
#
# Existing dependencies will be clobbered; when :force => true then all gems
# will be cleared first, otherwise only existing local dependencies of the
# gems to be installed will be removed.
#
# Examples:
#
# merb:dependencies:install # install all dependencies using stable strategy
# merb:dependencies:install stable --version 0.9.8 # install a specific version of the framework
# merb:dependencies:install stable missing # install currently missing gems locally
# merb:dependencies:install stable merb-more # install only merb-more related dependencies
# merb:dependencies:install stable --stack # install framework dependencies
# merb:dependencies:install stable --no-stack # install 3rd party dependencies
# merb:dependencies:install stable --config # read dependencies from the default config
# merb:dependencies:install stable --config-file file.yml # read from the specified config file
#
# In addition to the options above, edge install uses the following:
#
# merb:dependencies:install edge # install all dependencies using edge strategy
# merb:dependencies:install edge --sources file.yml # install edge from the specified git sources config
desc 'install [stable|edge] [comp]', 'Install application dependencies'
method_options "--sources" => :optional, # only for edge strategy
"--local" => :boolean, # force local install
"--dry-run" => :boolean,
"--force" => :boolean
def install(strategy = 'stable', comp = nil)
if strategy?(strategy)
# Force local dependencies by creating ./gems before proceeding
create_if_missing(default_gem_dir) if options[:local]
where = gem_dir ? 'locally' : 'system-wide'
# When comp == 'missing' then filter on missing dependencies
if only_missing = comp == 'missing'
message "Preparing to install missing gems #{where} using #{strategy} strategy..."
comp = nil
else
message "Preparing to install #{where} using #{strategy} strategy..."
end
# If comp given, filter on known stack components
deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies
self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir)
# Only install currently missing gems (for comp == missing)
if only_missing
deps.reject! { |dep| not missing.include?(dep) }
end
if deps.empty?
warning "No dependencies to install..."
else
puts "#{deps.length} dependencies to install..."
install_dependencies(strategy, deps)
end
# Show current dependency info now that we're done
puts # Seperate output
list('local', comp)
else
warning "Invalid install strategy '#{strategy}'"
puts
message "Please choose one of the following installation strategies: stable or edge:"
puts "$ thor merb:dependencies:install stable"
puts "$ thor merb:dependencies:install edge"
end
end
# Uninstall application dependencies.
#
# By default all required dependencies are installed. An optional argument
# can be used to filter on a set of known components.
#
# Existing dependencies will be clobbered; when :force => true then all gems
# will be cleared, otherwise only existing local dependencies of the
# matching component set will be removed.
#
# Examples:
#
# merb:dependencies:uninstall # uninstall all dependencies - the default
# merb:dependencies:uninstall merb-more # uninstall merb-more related gems locally
# merb:dependencies:uninstall --config # read dependencies from the default config
desc 'uninstall [comp]', 'Uninstall application dependencies'
method_options "--dry-run" => :boolean, "--force" => :boolean
def uninstall(comp = nil)
# If comp given, filter on known stack components
deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies
self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir)
# Clobber existing local dependencies - based on self.local
clobber_dependencies!
end
# Recreate binary gems on the current platform.
#
# This task should be executed as part of a deployment setup, where the
# deployment system runs this after the app has been installed.
# Usually triggered by Capistrano, God...
#
# It will regenerate gems from the bundled gems cache for any gem that has
# C extensions - which need to be recompiled for the target deployment platform.
#
# Note: gems/cache should be in your SCM for this to work correctly.
desc 'redeploy', 'Recreate any binary gems on the target platform'
method_options "--dry-run" => :boolean
def redeploy
require 'tempfile' # for Dir::tmpdir access
if gem_dir && File.directory?(cache_dir = File.join(gem_dir, 'cache'))
local_gemspecs.each do |gemspec|
unless gemspec.extensions.empty?
if File.exists?(gem_file = File.join(cache_dir, "#{gemspec.full_name}.gem"))
gem_file_copy = File.join(Dir::tmpdir, File.basename(gem_file))
if dry_run?
note "Recreating #{gemspec.full_name}"
else
message "Recreating #{gemspec.full_name}"
# Copy the gem to a temporary file, because otherwise RubyGems/FileUtils
# will complain about copying identical files (same source/destination).
FileUtils.cp(gem_file, gem_file_copy)
Merb::Gem.install(gem_file_copy, :install_dir => gem_dir)
File.delete(gem_file_copy)
end
end
end
end
else
error "No application local gems directory found"
end
end
# Create a dependencies configuration file.
#
# A configuration yaml file will be created from the extracted application
# dependencies. The format of the configuration is as follows:
#
# ---
# - merb-core (= 0.9.8, runtime)
# - merb-slices (= 0.9.8, runtime)
#
# This format is exactly the same as Gem::Dependency#to_s returns.
#
# Examples:
#
# merb:dependencies:configure --force # overwrite the default config file
# merb:dependencies:configure --version 0.9.8 # configure specific framework version
# merb:dependencies:configure --config-file file.yml # write to the specified config file
desc 'configure [comp]', 'Create a dependencies config file'
method_options "--dry-run" => :boolean, "--force" => :boolean
def configure(comp = nil)
# If comp given, filter on known stack components
deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies
config = YAML.dump(deps.map { |d| d.to_s })
puts "#{config}\n"
if File.exists?(config_file) && !options[:force]
error "File already exists! Use --force to overwrite."
else
if dry_run?
note "Written #{config_file}"
else
FileUtils.mkdir_p(config_dir) unless File.directory?(config_dir)
File.open(config_file, 'w') { |f| f.write config }
success "Written #{config_file}"
end
end
rescue
error "Failed to write to #{config_file}"
end
### Helper Methods
def strategy?(strategy)
if self.respond_to?(method = :"#{strategy}_strategy", true)
method
end
end
def install_dependencies(strategy, deps)
if method = strategy?(strategy)
# Clobber existing local dependencies
clobber_dependencies!
# Run the chosen strategy - collect files installed from stable gems
installed_from_stable = send(method, deps).map { |d| d.name }
# Sleep a bit otherwise the following steps won't see the new files
sleep(deps.length) if deps.length > 0
# Leave a file to denote the strategy that has been used for this dependency
self.local.each do |spec|
next unless File.directory?(spec.full_gem_path)
unless installed_from_stable.include?(spec.name)
FileUtils.touch(File.join(spec.full_gem_path, "#{strategy}.strategy"))
else
FileUtils.touch(File.join(spec.full_gem_path, "stable.strategy"))
end
end
# Ask for system installation of minigem - needs sudo...
if deps.find { |d| d.name == 'minigems' }
info "Installing minigems.rb on your system..."
`#{sudo} minigem install`
end
# Add local binaries for the installed framework dependencies
comps = Merb::Stack.all_components & deps.map { |d| d.name }
comps << { :no_minigems => 'merb-gen' }
ensure_bin_wrapper_for(*comps)
return true
end
false
end
def dependencies
if use_config?
# Use preconfigured dependencies from yaml file
deps = config_dependencies
else
# Extract dependencies from the current application
deps = Merb::Stack.core_dependencies(gem_dir, ignore_dependencies?)
deps += Merb::Dependencies.extract_dependencies(working_dir)
end
stack_components = Merb::Stack.components
if options[:stack]
# Limit to stack components only
deps.reject! { |dep| not stack_components.include?(dep.name) }
elsif options[:"no-stack"]
# Limit to non-stack components
deps.reject! { |dep| stack_components.include?(dep.name) }
end
if options[:version]
version_req = ::Gem::Requirement.create("= #{options[:version]}")
elsif core = deps.find { |d| d.name == 'merb-core' }
version_req = core.version_requirements
end
if version_req
# Handle specific version requirement for framework components
framework_components = Merb::Stack.framework_components
deps.each do |dep|
if framework_components.include?(dep.name)
dep.version_requirements = version_req
end
end
end
deps
end
def config_dependencies
if File.exists?(config_file)
self.class.parse_dependencies_yaml(File.read(config_file))
else
[]
end
end
def use_config?
options[:config] || options[:"config-file"]
end
def config_file
@config_file ||= begin
options[:"config-file"] || File.join(working_dir, 'config', 'dependencies.yml')
end
end
def config_dir
File.dirname(config_file)
end
### Strategy handlers
private
def stable_strategy(deps)
installed_from_rubygems = []
if core = deps.find { |d| d.name == 'merb-core' }
if dry_run?
note "Installing #{core.name}..."
else
if install_dependency(core)
installed_from_rubygems << core
else
msg = "Try specifying a lower version of merb-core with --version"
if version_no = core.version_requirements.to_s[/([\.\d]+)$/, 1]
num = "%03d" % (version_no.gsub('.', '').to_i - 1)
puts "The required version (#{version_no}) probably isn't available as a stable rubygem yet."
info "#{msg} #{num.split(//).join('.')}"
else
puts "The required version probably isn't available as a stable rubygem yet."
info msg
end
end
end
end
deps.each do |dependency|
next if dependency.name == 'merb-core'
if dry_run?
note "Installing #{dependency.name}..."
else
install_dependency(dependency)
installed_from_rubygems << dependency
end
end
installed_from_rubygems
end
def edge_strategy(deps)
installed_from_rubygems = []
# Selectively update repositories for the matching dependencies
update_dependency_repositories(deps) unless dry_run?
# Skip gem dependencies to prevent them from being installed from stable;
# however, core dependencies will be retrieved from source when available
install_opts = { :ignore_dependencies => true }
if core = deps.find { |d| d.name == 'merb-core' }
if dry_run?
note "Installing #{core.name}..."
else
if install_dependency_from_source(core, install_opts)
elsif install_dependency(core, install_opts)
info "Installed #{core.name} from rubygems..."
installed_from_rubygems << core
end
end
end
deps.each do |dependency|
next if dependency.name == 'merb-core'
if dry_run?
note "Installing #{dependency.name}..."
else
if install_dependency_from_source(dependency, install_opts)
elsif install_dependency(dependency, install_opts)
info "Installed #{dependency.name} from rubygems..."
installed_from_rubygems << dependency
end
end
end
installed_from_rubygems
end
### Class Methods
public
def self.list(filter = 'all', comp = nil, options = {})
instance = Merb::Dependencies.new
instance.options = options
instance.list(filter, comp)
end
# Extract application dependencies by querying the app directly.
def self.extract_dependencies(merb_root, env = 'production')
require 'merb-core'
if !@_merb_loaded || Merb.root != merb_root
Merb.start_environment(
:testing => true,
:adapter => 'runner',
:environment => env,
:merb_root => merb_root
)
@_merb_loaded = true
end
Merb::BootLoader::Dependencies.dependencies
rescue => e
error "Couldn't extract dependencies from application!"
error e.message
p e.backtrace
puts "Make sure you're executing the task from your app (--merb-root), or"
puts "specify a config option (--config or --config-file=YAML_FILE)"
return []
end
# Parse the basic YAML config data, and process Gem::Dependency output.
# Formatting example: merb_helpers (>= 0.9.8, runtime)
def self.parse_dependencies_yaml(yaml)
dependencies = []
entries = YAML.load(yaml) rescue []
entries.each do |entry|
if matches = entry.match(/^(\S+) \(([^,]+)?, ([^\)]+)\)/)
name, version_req, type = matches.captures
dependencies << ::Gem::Dependency.new(name, version_req, type.to_sym)
else
error "Invalid entry: #{entry}"
end
end
dependencies
end
end
class Stack < Thor
# The Stack tasks will install dependencies based on known sets of gems,
# regardless of actual application dependency settings.
DM_STACK = %w[
dm-core
dm-aggregates
dm-migrations
dm-timestamps
dm-types
dm-validations
]
MERB_STACK = %w[
minigems
merb-core
merb-action-args
merb-assets
merb-cache
merb-helpers
merb-mailer
merb-slices
merb-auth
] + DM_STACK
MERB_BASICS = %w[
minigems
merb-core
merb-action-args
merb-assets
merb-cache
merb-helpers
merb-mailer
merb-slices
]
# The following sets are meant for repository lookup; unlike the sets above
# these correspond to specific git repository items.
MERB_MORE = %w[
merb-action-args
merb-assets
merb-auth
merb-auth-core
merb-auth-more
merb-auth-slice-password
merb-cache
merb-exceptions
merb-gen
merb-haml
merb-helpers
merb-mailer
merb-param-protection
merb-slices
merb_datamapper
]
MERB_PLUGINS = %w[
merb_activerecord
merb_builder
merb_jquery
merb_laszlo
merb_parts
merb_screw_unit
merb_sequel
merb_stories
merb_test_unit
]
DM_MORE = %w[
dm-adjust
dm-aggregates
dm-ar-finders
dm-cli
dm-constraints
dm-is-example
dm-is-list
dm-is-nested_set
dm-is-remixable
dm-is-searchable
dm-is-state_machine
dm-is-tree
dm-is-versioned
dm-migrations
dm-observer
dm-querizer
dm-serializer
dm-shorthand
dm-sweatshop
dm-tags
dm-timestamps
dm-types
dm-validations
dm-couchdb-adapter
dm-ferret-adapter
dm-rest-adapter
]
attr_accessor :system, :local, :missing
include MerbThorHelper
global_method_options = {
"--merb-root" => :optional, # the directory to operate on
"--include-dependencies" => :boolean, # gather sub-dependencies
"--version" => :optional # gather specific version of framework
}
method_options global_method_options
def initialize(*args); super; end
# List components and their dependencies.
#
# Examples:
#
# merb:stack:list # list all standard stack components
# merb:stack:list all # list all component sets
# merb:stack:list merb-more # list all dependencies of merb-more
desc 'list [all|comp]', 'List available components (optionally filtered, defaults to merb stack)'
def list(comp = 'stack')
if comp == 'all'
Merb::Stack.component_sets.keys.sort.each do |comp|
unless (components = Merb::Stack.component_sets[comp]).empty?
message "Dependencies for '#{comp}' set:"
components.each { |c| puts "- #{c}" }
end
end
else
message "Dependencies for '#{comp}' set:"
Merb::Stack.components(comp).each { |c| puts "- #{c}" }
end
end
# Install stack components or individual gems - from stable rubygems by default.
#
# See also: Merb::Dependencies#install and Merb::Dependencies#install_dependencies
#
# Examples:
#
# merb:stack:install # install the default merb stack
# merb:stack:install basics # install a basic set of dependencies
# merb:stack:install merb-core # install merb-core from stable
# merb:stack:install merb-more --edge # install merb-core from edge
# merb:stack:install merb-core thor merb-slices # install the specified gems
desc 'install [COMP, ...]', 'Install stack components'
method_options "--edge" => :boolean,
"--sources" => :optional,
"--force" => :boolean,
"--dry-run" => :boolean,
"--strategy" => :optional
def install(*comps)
mngr = self.dependency_manager
deps = gather_dependencies(comps)
mngr.system, mngr.local, mngr.missing = Merb::Gem.partition_dependencies(deps, gem_dir)
mngr.install_dependencies(strategy, deps)
end
# Uninstall stack components or individual gems.
#
# See also: Merb::Dependencies#uninstall
#
# Examples:
# merb:stack:uninstall # uninstall the default merb stack
# merb:stack:uninstall merb-more # uninstall merb-more
# merb:stack:uninstall merb-core thor merb-slices # uninstall the specified gems
desc 'uninstall [COMP, ...]', 'Uninstall stack components'
method_options "--dry-run" => :boolean, "--force" => :boolean
def uninstall(*comps)
deps = gather_dependencies(comps)
self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir)
# Clobber existing local dependencies - based on self.local
clobber_dependencies!
end
protected
def gather_dependencies(comps = [])
if comps.empty?
gems = MERB_STACK
else
gems = comps.map { |c| Merb::Stack.components(c) }.flatten
end
version_req = if options[:version]
::Gem::Requirement.create(options[:version])
end
framework_components = Merb::Stack.framework_components
gems.map do |gem|
if version_req && framework_components.include?(gem)
::Gem::Dependency.new(gem, version_req)
else
::Gem::Dependency.new(gem, ::Gem::Requirement.default)
end
end
end
def strategy
options[:strategy] || (options[:edge] ? 'edge' : 'stable')
end
def dependency_manager
@_dependency_manager ||= begin
instance = Merb::Dependencies.new
instance.options = options
instance
end
end
public
def self.repository_sets
@_repository_sets ||= begin
# the component itself as a fallback
comps = Hash.new { |(hsh,c)| [c] }
# git repository based component sets
comps["merb"] = ["merb-core"] + MERB_MORE
comps["merb-more"] = MERB_MORE.sort
comps["merb-plugins"] = MERB_PLUGINS.sort
comps["dm-more"] = DM_MORE.sort
comps
end
end
def self.component_sets
@_component_sets ||= begin
# the component itself as a fallback
comps = Hash.new { |(hsh,c)| [c] }
comps.update(repository_sets)
# specific set of dependencies
comps["stack"] = MERB_STACK.sort
comps["basics"] = MERB_BASICS.sort
# orm dependencies
comps["datamapper"] = DM_STACK.sort
comps["sequel"] = ["merb_sequel", "sequel"]
comps["activerecord"] = ["merb_activerecord", "activerecord"]
comps
end
end
def self.framework_components
%w[merb-core merb-more merb-plugins].inject([]) do |all, comp|
all + components(comp)
end
end
def self.components(comp = nil)
if comp
component_sets[comp]
else
comps = %w[merb-core merb-more merb-plugins dm-core dm-more]
comps.inject([]) do |all, grp|
all + (component_sets[grp] || [])
end
end
end
def self.select_component_dependencies(dependencies, comp = nil)
comps = components(comp) || []
dependencies.select { |dep| comps.include?(dep.name) }
end
def self.base_components
%w[thor rake]
end
def self.all_components
base_components + framework_components
end
# Find the latest merb-core and gather its dependencies.
# We check for 0.9.8 as a minimum release version.
def self.core_dependencies(gem_dir = nil, ignore_deps = false)
@_core_dependencies ||= begin
if gem_dir # add local gems to index
::Gem.clear_paths; ::Gem.path.unshift(gem_dir)
end
deps = []
merb_core = ::Gem::Dependency.new('merb-core', '>= 0.9.8')
if gemspec = ::Gem.source_index.search(merb_core).last
deps << ::Gem::Dependency.new('merb-core', gemspec.version)
if ignore_deps
deps += gemspec.dependencies.select do |d|
base_components.include?(d.name)
end
else
deps += gemspec.dependencies
end
end
::Gem.clear_paths if gem_dir # reset
deps
end
end
def self.lookup_repository_name(item)
set_name = nil
# The merb repo contains -more as well, so it needs special attention
return 'merb' if self.repository_sets['merb'].include?(item)
# Proceed with finding the item in a known component set
self.repository_sets.find do |set, items|
next if set == 'merb'
items.include?(item) ? (set_name = set) : nil
end
set_name
end
end
class Tasks < Thor
include MerbThorHelper
# Show merb.thor version information
#
# merb:tasks:version # show the current version info
# merb:tasks:version --info # show extended version info
desc 'version', 'Show verion info'
method_options "--info" => :boolean
def version
message "Currently installed merb.thor version: #{MERB_THOR_VERSION}"
if options[:version]
self.options = { :"dry-run" => true }
self.update # run update task with dry-run enabled
end
end
# Update merb.thor tasks from remotely available version
#
# merb:tasks:update # update merb.thor
# merb:tasks:update --force # force-update merb.thor
# merb:tasks:update --dry-run # show version info only
desc 'update [URL]', 'Fetch the latest merb.thor and install it locally'
method_options "--dry-run" => :boolean, "--force" => :boolean
def update(url = 'http://merbivore.com/merb.thor')
require 'open-uri'
require 'rubygems/version'
remote_file = open(url)
code = remote_file.read
# Extract version information from the source code
if version = code[/^MERB_THOR_VERSION\s?=\s?('|")([\.\d]+)('|")/,2]
# borrow version comparison from rubygems' Version class
current_version = ::Gem::Version.new(MERB_THOR_VERSION)
remote_version = ::Gem::Version.new(version)
if current_version >= remote_version
puts "currently installed: #{current_version}"
if current_version != remote_version
puts "available version: #{remote_version}"
end
info "No update of merb.thor necessary#{options[:force] ? ' (forced)' : ''}"
proceed = options[:force]
elsif current_version < remote_version
puts "currently installed: #{current_version}"
puts "available version: #{remote_version}"
proceed = true
end
if proceed && !dry_run?
File.open(File.join(__FILE__), 'w') do |f|
f.write(code)
end
success "Installed the latest merb.thor (v#{version})"
end
else
raise "invalid source-code data"
end
rescue OpenURI::HTTPError
error "Error opening #{url}"
rescue => e
error "An error occurred (#{e.message})"
end
end
#### MORE LOW-LEVEL TASKS ####
class Gem < Thor
group 'core'
include MerbThorHelper
extend GemManagement
attr_accessor :system, :local, :missing
global_method_options = {
"--merb-root" => :optional, # the directory to operate on
"--version" => :optional, # gather specific version of gem
"--ignore-dependencies" => :boolean # don't install sub-dependencies
}
method_options global_method_options
def initialize(*args); super; end
# List gems that match the specified criteria.
#
# By default all local gems are listed. When the first argument is 'all' the
# list is partitioned into system an local gems; specify 'system' to show
# only system gems. A second argument can be used to filter on a set of known
# components, like all merb-more gems for example.
#
# Examples:
#
# merb:gem:list # list all local gems - the default
# merb:gem:list all # list system and local gems
# merb:gem:list system # list only system gems
# merb:gem:list all merb-more # list only merb-more related gems
# merb:gem:list --version 0.9.8 # list gems that match the version
desc 'list [all|local|system] [comp]', 'Show installed gems'
def list(filter = 'local', comp = nil)
deps = comp ? Merb::Stack.select_component_dependencies(dependencies, comp) : dependencies
self.system, self.local, self.missing = Merb::Gem.partition_dependencies(deps, gem_dir)
case filter
when 'all'
message 'Installed system gems:'
display_gemspecs(system)
message 'Installed local gems:'
display_gemspecs(local)
when 'system'
message 'Installed system gems:'
display_gemspecs(system)
when 'local'
message 'Installed local gems:'
display_gemspecs(local)
else
warning "Invalid listing filter '#{filter}'"
end
end
# Install the specified gems.
#
# All arguments should be names of gems to install.
#
# When :force => true then any existing versions of the gems to be installed
# will be uninstalled first. It's important to note that so-called meta-gems
# or gems that exactly match a set of Merb::Stack.components will have their
# sub-gems uninstalled too. For example, uninstalling merb-more will install
# all contained gems: merb-action-args, merb-assets, merb-gen, ...
#
# Examples:
#
# merb:gem:install merb-core merb-slices # install all specified gems
# merb:gem:install merb-core --version 0.9.8 # install a specific version of a gem
# merb:gem:install merb-core --force # uninstall then subsequently install the gem
# merb:gem:install merb-core --cache # try to install locally from system gems
# merb:gem:install merb-core --binaries # also install adapted bin wrapper
desc 'install GEM_NAME [GEM_NAME, ...]', 'Install a gem from rubygems'
method_options "--cache" => :boolean,
"--binaries" => :boolean,
"--dry-run" => :boolean,
"--force" => :boolean
def install(*names)
self.include_dependencies = true # deal with dependencies by default
opts = { :version => options[:version], :cache => options[:cache] }
current_gem = nil
# uninstall existing gems of the ones we're going to install
uninstall(*names) if options[:force]
names.each do |gem_name|
current_gem = gem_name
if dry_run?
note "Installing #{current_gem}..."
else
message "Installing #{current_gem}..."
self.class.install(gem_name, default_install_options.merge(opts))
ensure_bin_wrapper_for(gem_name) if options[:binaries]
end
end
rescue => e
error "Failed to install #{current_gem ? current_gem : 'gem'} (#{e.message})"
end
# Uninstall the specified gems.
#
# By default all specified gems are uninstalled. It's important to note that
# so-called meta-gems or gems that match a set of Merb::Stack.components will
# have their sub-gems uninstalled too. For example, uninstalling merb-more
# will install all contained gems: merb-action-args, merb-assets, ...
#
# Existing dependencies will be clobbered; when :force => true then all gems
# will be cleared, otherwise only existing local dependencies of the
# matching component set will be removed.
#
# Examples:
#
# merb:gem:uninstall merb-core merb-slices # uninstall all specified gems
# merb:gem:uninstall merb-core --version 0.9.8 # uninstall a specific version of a gem
desc 'uninstall GEM_NAME [GEM_NAME, ...]', 'Unstall a gem'
method_options "--dry-run" => :boolean
def uninstall(*names)
self.include_dependencies = true # deal with dependencies by default
opts = { :version => options[:version] }
current_gem = nil
if dry_run?
note "Uninstalling any existing gems of: #{names.join(', ')}"
else
message "Uninstalling any existing gems of: #{names.join(', ')}"
names.each do |gem_name|
current_gem = gem_name
Merb::Gem.uninstall(gem_name, default_uninstall_options) rescue nil
# if this gem is a meta-gem or a component set name, remove sub-gems
(Merb::Stack.components(gem_name) || []).each do |comp|
Merb::Gem.uninstall(comp, default_uninstall_options) rescue nil
end
end
end
rescue => e
error "Failed to uninstall #{current_gem ? current_gem : 'gem'} (#{e.message})"
end
private
# Return dependencies for all installed gems; both system-wide and locally;
# optionally filters on :version requirement.
def dependencies
version_req = if options[:version]
::Gem::Requirement.create(options[:version])
else
::Gem::Requirement.default
end
if gem_dir
::Gem.clear_paths; ::Gem.path.unshift(gem_dir)
::Gem.source_index.refresh!
end
deps = []
::Gem.source_index.each do |fullname, gemspec|
if version_req.satisfied_by?(gemspec.version)
deps << ::Gem::Dependency.new(gemspec.name, "= #{gemspec.version}")
end
end
::Gem.clear_paths if gem_dir
deps.sort
end
public
# Install gem with some default options.
def self.install(name, options = {})
defaults = {}
defaults[:cache] = false unless opts[:install_dir]
install_gem(name, defaults.merge(options))
end
# Uninstall gem with some default options.
def self.uninstall(name, options = {})
defaults = { :ignore => true, :executables => true }
uninstall_gem(name, defaults.merge(options))
end
end
class Source < Thor
group 'core'
include MerbThorHelper
extend GemManagement
attr_accessor :system, :local, :missing
global_method_options = {
"--merb-root" => :optional, # the directory to operate on
"--include-dependencies" => :boolean, # gather sub-dependencies
"--sources" => :optional # a yml config to grab sources from
}
method_options global_method_options
def initialize(*args); super; end
# List source repositories, of either local or known sources.
#
# Examples:
#
# merb:source:list # list all local sources
# merb:source:list available # list all known sources
desc 'list [local|available]', 'Show git source repositories'
def list(mode = 'local')
if mode == 'available'
message 'Available source repositories:'
repos = self.class.repos(options[:sources])
repos.keys.sort.each { |name| puts "- #{name}: #{repos[name]}" }
elsif mode == 'local'
message 'Current source repositories:'
Dir[File.join(source_dir, '*')].each do |src|
next unless File.directory?(src)
src_name = File.basename(src)
unless (repos = source_manager.existing_repos(src_name)).empty?
puts "#{src_name}"
repos.keys.sort.each { |b| puts "- #{b}: #{repos[b]}" }
end
end
else
error "Unknown listing: #{mode}"
end
end
# Install the specified gems.
#
# All arguments should be names of gems to install.
#
# When :force => true then any existing versions of the gems to be installed
# will be uninstalled first. It's important to note that so-called meta-gems
# or gems that exactly match a set of Merb::Stack.components will have their
# sub-gems uninstalled too. For example, uninstalling merb-more will install
# all contained gems: merb-action-args, merb-assets, merb-gen, ...
#
# Examples:
#
# merb:source:install merb-core merb-slices # install all specified gems
# merb:source:install merb-core --force # uninstall then subsequently install the gem
# merb:source:install merb-core --wipe # clear repo then install the gem
# merb:source:install merb-core --binaries # also install adapted bin wrapper
desc 'install GEM_NAME [GEM_NAME, ...]', 'Install a gem from git source/edge'
method_options "--binaries" => :boolean,
"--dry-run" => :boolean,
"--force" => :boolean,
"--wipe" => :boolean
def install(*names)
# uninstall existing gems of the ones we're going to install
uninstall(*names) if options[:force] || options[:wipe]
# We want dependencies instead of just names
deps = names.map { |n| ::Gem::Dependency.new(n, ::Gem::Requirement.default) }
# Selectively update repositories for the matching dependencies
update_dependency_repositories(deps) unless dry_run?
current_gem = nil
deps.each do |dependency|
current_gem = dependency.name
if dry_run?
note "Installing #{current_gem} from source..."
else
message "Installing #{current_gem} from source..."
if install_dependency_from_source(dependency)
ensure_bin_wrapper_for(dependency.name) if options[:binaries]
end
end
end
rescue => e
error "Failed to install #{current_gem ? current_gem : 'gem'} (#{e.message})"
end
# Uninstall the specified gems.
#
# By default all specified gems are uninstalled. It's important to note that
# so-called meta-gems or gems that match a set of Merb::Stack.components will
# have their sub-gems uninstalled too. For example, uninstalling merb-more
# will install all contained gems: merb-action-args, merb-assets, ...
#
# Existing dependencies will be clobbered; when :force => true then all gems
# will be cleared, otherwise only existing local dependencies of the
# matching component set will be removed. Additionally when :wipe => true,
# the matching git repositories will be removed from the source directory.
#
# Examples:
#
# merb:source:uninstall merb-core merb-slices # uninstall all specified gems
# merb:source:uninstall merb-core --wipe # force-uninstall a gem and clear repo
desc 'uninstall GEM_NAME [GEM_NAME, ...]', 'Unstall a gem (specify --force to remove the repo)'
method_options "--version" => :optional, "--dry-run" => :boolean, "--wipe" => :boolean
def uninstall(*names)
# Remove the repos that contain the gem
if options[:wipe]
extract_repositories(names).each do |(name, url)|
if File.directory?(src = File.join(source_dir, name))
if dry_run?
note "Removing #{src}..."
else
info "Removing #{src}..."
FileUtils.rm_rf(src)
end
end
end
end
# Use the Merb::Gem#uninstall task to handle this
gem_tasks = Merb::Gem.new
gem_tasks.options = options
gem_tasks.uninstall(*names)
end
# Update the specified source repositories.
#
# The arguments can be actual repository names (from Merb::Source.repos)
# or names of known merb stack gems. If the repo doesn't exist already,
# it will be created and cloned.
#
# merb:source:pull merb-core # update source of specified gem
# merb:source:pull merb-slices # implicitly updates merb-more
desc 'pull REPO_NAME [GEM_NAME, ...]', 'Update git source repository from edge'
def pull(*names)
repos = extract_repositories(names)
update_repositories(repos)
unless repos.empty?
message "Updated the following repositories:"
repos.each { |name, url| puts "- #{name}: #{url}" }
else
warning "No repositories found to update!"
end
end
# Clone a git repository into ./src.
# The repository can be a direct git url or a known -named- repository.
#
# Examples:
#
# merb:source:clone merb-core
# merb:source:clone dm-core awesome-repo
# merb:source:clone dm-core --sources ./path/to/sources.yml
# merb:source:clone git://github.com/sam/dm-core.git
desc 'clone (REPO_NAME|URL) [DIR_NAME]', 'Clone git source repository by name or url'
def clone(repository, name = nil)
if repository =~ /^git:\/\//
repository_url = repository
repository_name = File.basename(repository_url, '.git')
elsif url = Merb::Source.repo(repository, options[:sources])
repository_url = url
repository_name = repository
end
source_manager.clone(name || repository_name, repository_url)
end
# Git repository sources - pass source_config option to load a yaml
# configuration file - defaults to ./config/git-sources.yml and
# ~/.merb/git-sources.yml - which you need to create yourself.
#
# Example of contents:
#
# merb-core: git://github.com/myfork/merb-core.git
# merb-more: git://github.com/myfork/merb-more.git
def self.repos(source_config = nil)
source_config ||= begin
local_config = File.join(Dir.pwd, 'config', 'git-sources.yml')
user_config = File.join(ENV["HOME"] || ENV["APPDATA"], '.merb', 'git-sources.yml')
File.exists?(local_config) ? local_config : user_config
end
if source_config && File.exists?(source_config)
default_repos.merge(YAML.load(File.read(source_config)))
else
default_repos
end
end
def self.repo(name, source_config = nil)
self.repos(source_config)[name]
end
# Default Git repositories
def self.default_repos
@_default_repos ||= {
'merb' => "git://github.com/wycats/merb.git",
'merb-plugins' => "git://github.com/wycats/merb-plugins.git",
'extlib' => "git://github.com/sam/extlib.git",
'dm-core' => "git://github.com/sam/dm-core.git",
'dm-more' => "git://github.com/sam/dm-more.git",
'sequel' => "git://github.com/wayneeseguin/sequel.git",
'do' => "git://github.com/sam/do.git",
'thor' => "git://github.com/wycats/thor.git",
'rake' => "git://github.com/jimweirich/rake.git",
'minigems' => "git://github.com/fabien/minigems.git"
}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment