This repository has been archived on 2020-05-27. You can view files and clone it, but cannot push or open issues/pull-requests.
binaryattraction/merb.thor

823 lines
28 KiB
Ruby

#!/usr/bin/env ruby
require 'rubygems'
require 'rubygems/dependency_installer'
require 'rubygems/uninstaller'
require 'thor'
require 'fileutils'
require 'yaml'
module MerbThorHelper
private
# 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, it means we are in a Merb app.
def application?
gem_dir
end
# If a local ./gems dir is found, return it.
def gem_dir
if File.directory?(dir = File.join(working_dir, 'gems'))
dir
end
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
# Create a modified executable wrapper in the app's ./bin directory.
def ensure_local_bin_for(*gems)
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)
spec.executables.each do |exec|
if File.exists?(executable = File.join(gem_dir, 'bin', exec))
local_executable = File.join(bin_dir, exec)
puts "Adding local executable #{local_executable}"
File.open(local_executable, 'w', 0755) do |f|
f.write(executable_wrapper(spec, exec))
end
end
end
end
end
end
end
def executable_wrapper(spec, bin_file_name)
<<-TEXT
#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}
#
# This file was generated by merb.thor.
#
# The application '#{spec.name}' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
if 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
end
# TODO
# - a task to figure out an app's dependencies
# - pulling a specific UUID/Tag (gitspec hash) with clone/update
# - a 'deploy' task (in addition to 'redeploy' ?)
# - eventually take a --orm option for the 'merb-stack' type of tasks
class Merb < Thor
class SourcePathMissing < Exception
end
class GemPathMissing < Exception
end
class GemInstallError < Exception
end
class GemUninstallError < Exception
end
# Install a Merb stack from stable RubyForge gems. Optionally install a
# suitable Rack adapter/server when setting --adapter to one of the
# following: mongrel, emongrel, thin or ebb.
desc 'stable', 'Install extlib, merb-core and merb-more from rubygems'
method_options "--merb-root" => :optional,
"--adapter" => :optional
def stable
adapters = %w[mongrel emongrel thin ebb]
stable = Stable.new
stable.options = options
if stable.core && stable.more
puts "Installed extlib, merb-core and merb-more"
if options[:adapter] && adapters.include?(options[:adapter]) &&
stable.refresh_from_gems(options[:adapter])
puts "Installed #{options[:adapter]}"
elsif options[:adapter]
puts "Please specify one of the following adapters: #{adapters.join(' ')}"
end
end
end
class Stable < Thor
# The Stable tasks deal with known -stable- gems; available
# as shortcuts to Merb and DataMapper gems.
#
# These are pulled from rubyforge and installed into into the
# desired gems dir (either system-wide or into the application's
# gems directory).
include MerbThorHelper
# Gets latest gem versions from RubyForge and installs them.
#
# Examples:
#
# thor merb:edge:core
# thor merb:edge:core --merb-root ./path/to/your/app
# thor merb:edge:core --sources ./path/to/sources.yml
desc 'core', 'Install extlib and merb-core from git HEAD'
method_options "--merb-root" => :optional
def core
refresh_from_gems 'extlib', 'merb-core'
ensure_local_bin_for('merb-core', 'rake', 'rspec', 'thor')
end
desc 'more', 'Install merb-more from rubygems'
method_options "--merb-root" => :optional
def more
refresh_from_gems 'merb-more'
ensure_local_bin_for('merb-gen')
end
desc 'plugins', 'Install merb-plugins from rubygems'
method_options "--merb-root" => :optional
def plugins
refresh_from_gems 'merb-plugins'
end
desc 'dm_core', 'Install dm-core from rubygems'
method_options "--merb-root" => :optional
def dm_core
refresh_from_gems 'extlib', 'dm-core'
end
desc 'dm_more', 'Install dm-more from rubygems'
method_options "--merb-root" => :optional
def dm_more
refresh_from_gems 'extlib', 'dm-core', 'dm-more'
end
# Pull from RubyForge and install.
def refresh_from_gems(*components)
gems = Gems.new
gems.options = options
components.all? { |name| gems.install(name) }
end
end
# Retrieve latest Merb versions from git and optionally install them.
#
# Note: the --sources option takes a path to a YAML file
# with a regular Hash mapping gem names to git urls.
#
# Examples:
#
# thor merb:edge
# thor merb:edge --install
# thor merb:edge --merb-root ./path/to/your/app
# thor merb:edge --sources ./path/to/sources.yml
desc 'edge', 'Install extlib, merb-core and merb-more from git HEAD'
method_options "--merb-root" => :optional,
"--sources" => :optional,
"--install" => :boolean
def edge
edge = Edge.new
edge.options = options
edge.core
edge.more
end
class Edge < Thor
# The Edge tasks deal with known gems from the bleeding edge; available
# as shortcuts to Merb and DataMapper gems.
#
# These are pulled from git and optionally installed into into the
# desired gems dir (either system-wide or into the application's
# gems directory).
include MerbThorHelper
# Gets latest gem versions from git - optionally installs them.
#
# Note: the --sources option takes a path to a YAML file
# with a regular Hash mapping gem names to git urls,
# allowing pulling forks of the official repositories.
#
# Examples:
#
# thor merb:edge:core
# thor merb:edge:core --install
# thor merb:edge:core --merb-root ./path/to/your/app
# thor merb:edge:core --sources ./path/to/sources.yml
desc 'core', 'Update extlib and merb-core from git HEAD'
method_options "--merb-root" => :optional,
"--sources" => :optional,
"--install" => :boolean
def core
refresh_from_source 'thor', 'extlib', 'merb-core'
ensure_local_bin_for('merb-core', 'rake', 'rspec', 'thor')
end
desc 'more', 'Update merb-more from git HEAD'
method_options "--merb-root" => :optional,
"--sources" => :optional,
"--install" => :boolean
def more
refresh_from_source 'merb-more'
ensure_local_bin_for('merb-gen')
end
desc 'plugins', 'Update merb-plugins from git HEAD'
method_options "--merb-root" => :optional,
"--sources" => :optional,
"--install" => :boolean
def plugins
refresh_from_source 'merb-plugins'
end
desc 'dm_core', 'Update dm-core from git HEAD'
method_options "--merb-root" => :optional,
"--sources" => :optional,
"--install" => :boolean
def dm_core
refresh_from_source 'extlib', 'dm-core'
end
desc 'dm_more', 'Update dm-more from git HEAD'
method_options "--merb-root" => :optional,
"--sources" => :optional,
"--install" => :boolean
def dm_more
refresh_from_source 'extlib', 'dm-core', 'dm-more'
end
private
# Pull from git and optionally install the resulting gems.
def refresh_from_source(*components)
source = Source.new
source.options = options
components.each do |name|
source.clone(name)
source.install(name) if options[:install]
end
end
end
class Source < Thor
# The Source tasks deal with gem source packages - mainly from github.
# Any directory inside ./src is regarded as a gem that can be packaged
# and installed from there into the desired gems dir (either system-wide
# or into the application's gems directory).
include MerbThorHelper
# Install a particular gem from source.
#
# If a local ./gems dir is found, or --merb-root is given
# the gems will be installed locally into that directory.
#
# Note that this task doesn't retrieve any (new) source from git;
# To update and install you'd execute the following two tasks:
#
# thor merb:source:update merb-core
# thor merb:source:install merb-core
#
# Alternatively, look at merb:edge and merb:edge:* with --install.
#
# Examples:
#
# thor merb:source:install merb-core
# thor merb:source:install merb-more
# thor merb:source:install merb-more/merb-slices
# thor merb:source:install merb-plugins/merb_helpers
# thor merb:source:install merb-core --merb-root ./path/to/your/app
desc 'install GEM_NAME', 'Install a rubygem from (git) source'
method_options "--merb-root" => :optional
def install(name)
puts "Installing #{name}..."
gem_src_dir = File.join(source_dir, name)
opts = {}
opts[:install_dir] = gem_dir if gem_dir
Merb.install_gem_from_src(gem_src_dir, opts)
rescue Merb::SourcePathMissing
puts "Missing rubygem source path: #{gem_src_dir}"
rescue Merb::GemPathMissing
puts "Missing rubygems path: #{gem_dir}"
rescue => e
puts "Failed to install #{name} (#{e.message})"
end
# Clone a git repository into ./src. The repository can be
# a direct git url or a known -named- repository.
#
# Examples:
#
# thor merb:source:clone dm-core
# thor merb:source:clone dm-core --sources ./path/to/sources.yml
# thor merb:source:clone git://github.com/sam/dm-core.git
desc 'clone REPOSITORY', 'Clone a git repository into ./src'
method_options "--sources" => :optional
def clone(repository)
if repository =~ /^git:\/\//
repository_url = repository
elsif url = Merb.repos(options[:sources])[repository]
repository_url = url
end
if repository_url
repository_name = repository_url[/([\w+|-]+)\.git/u, 1]
fork_name = repository_url[/.com\/+?(.+)\/.+\.git/u, 1]
local_repo_path = "#{source_dir}/#{repository_name}"
if File.directory?(local_repo_path)
puts "\n#{repository_name} repository exists, updating or branching instead of cloning..."
FileUtils.cd(local_repo_path) do
# to avoid conflicts we need to set a remote branch for non official repos
existing_repos = `git remote -v`.split("\n").map{|branch| branch.split(/\s+/)}
origin_repo_url = existing_repos.detect{ |r| r.first == "origin" }.last
# pull from the original repository - no branching needed
if repository_url == origin_repo_url
puts "Pulling from #{repository_url}"
system %{
git fetch
git checkout master
git rebase origin/master
}
# update and switch to a branch for a particular github fork
elsif existing_repos.map{ |r| r.last }.include?(repository_url)
puts "Switching to remote branch: #{fork_name}"
`git checkout -b #{fork_name} #{fork_name}/master`
`git rebase #{fork_name}/master`
# create a new remote branch for a particular github fork
else
puts "Add a new remote branch: #{fork_name}"
`git remote add -f #{fork_name} #{repository_url}`
`git checkout -b#{fork_name} #{fork_name}/master`
end
end
else
FileUtils.cd(source_dir) do
puts "\nCloning #{repository_name} repository from #{repository_url}..."
system("git clone --depth=1 #{repository_url} ")
end
end
else
puts "No valid repository url given"
end
end
# Update a specific gem source directory from git. See #clone.
desc 'update REPOSITORY_URL', 'Update a git repository in ./src'
alias :update :clone
# Update all gem sources from git - based on the current branch.
desc 'refresh', 'Pull fresh copies of all source gems'
def refresh
repos = Dir["#{source_dir}/*"]
repos.each do |repo|
next unless File.directory?(repo) && File.exists?(File.join(repo, '.git'))
FileUtils.cd(repo) do
puts "Refreshing #{File.basename(repo)}"
branch = `git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1) /'`[/\* (.+)/, 1]
system %{git rebase #{branch}}
end
end
end
end
class Gems < Thor
# The Gems tasks deal directly with rubygems, either through remotely
# available sources (rubyforge for example) or by searching the
# system-wide gem cache for matching gems. The gems are installed from
# there into the desired gems dir (either system-wide or into the
# application's gems directory).
include MerbThorHelper
# Install a gem and its dependencies.
#
# If a local ./gems dir is found, or --merb-root is given
# the gems will be installed locally into that directory.
#
# The option --cache will look in the system's gem cache
# for the latest version and install it in the apps' gems.
# This is particularly handy for gems that aren't available
# through rubyforge.org - like in-house merb slices etc.
#
# Examples:
#
# thor merb:gems:install merb-core
# thor merb:gems:install merb-core --cache
# thor merb:gems:install merb-core --version 0.9.7
# thor merb:gems:install merb-core --merb-root ./path/to/your/app
desc 'install GEM_NAME', 'Install a gem from rubygems'
method_options "--version" => :optional,
"--merb-root" => :optional,
"--cache" => :boolean
def install(name)
puts "Installing #{name}..."
opts = {}
opts[:version] = options[:version]
opts[:cache] = options[:cache] if gem_dir
opts[:install_dir] = gem_dir if gem_dir
Merb.install_gem(name, opts)
rescue => e
puts "Failed to install #{name} (#{e.message})"
end
# Update a gem and its dependencies.
#
# If a local ./gems dir is found, or --merb-root is given
# the gems will be installed locally into that directory.
#
# The option --cache will look in the system's gem cache
# for the latest version and install it in the apps' gems.
# This is particularly handy for gems that aren't available
# through rubyforge.org - like in-house merb slices etc.
#
# Examples:
#
# thor merb:gems:update merb-core
# thor merb:gems:update merb-core --cache
# thor merb:gems:update merb-core --merb-root ./path/to/your/app
desc 'update GEM_NAME', 'Update a gem from rubygems'
method_options "--merb-root" => :optional,
"--cache" => :boolean
def update(name)
puts "Updating #{name}..."
opts = {}
if gem_dir
if gemspec_path = Dir[File.join(gem_dir, 'specifications', "#{name}-*.gemspec")].last
gemspec = Gem::Specification.load(gemspec_path)
opts[:version] = Gem::Requirement.new [">#{gemspec.version}"]
end
opts[:install_dir] = gem_dir
opts[:cache] = options[:cache]
end
Merb.install_gem(name, opts)
rescue => e
puts "Failed to update #{name} (#{e.message})"
end
# Uninstall a gem - ignores dependencies.
#
# If a local ./gems dir is found, or --merb-root is given
# the gems will be uninstalled locally from that directory.
#
# Examples:
#
# thor merb:gems:uninstall merb-core
# thor merb:gems:uninstall merb-core --all
# thor merb:gems:uninstall merb-core --version 0.9.7
# thor merb:gems:uninstall merb-core --merb-root ./path/to/your/app
desc 'install GEM_NAME', 'Install a gem from rubygems'
desc 'uninstall GEM_NAME', 'Uninstall a gem'
method_options "--version" => :optional,
"--merb-root" => :optional,
"--all" => :boolean
def uninstall(name)
puts "Uninstalling #{name}..."
opts = {}
opts[:ignore] = true
opts[:all] = options[:all]
opts[:executables] = true
opts[:version] = options[:version]
opts[:install_dir] = gem_dir if gem_dir
Merb.uninstall_gem(name, opts)
rescue => e
puts "Failed to uninstall #{name} (#{e.message})"
end
# Completely remove a gem and all its versions - ignores dependencies.
#
# If a local ./gems dir is found, or --merb-root is given
# the gems will be uninstalled locally from that directory.
#
# Examples:
#
# thor merb:gems:wipe merb-core
# thor merb:gems:wipe merb-core --merb-root ./path/to/your/app
desc 'wipe GEM_NAME', 'Remove a gem completely'
method_options "--merb-root" => :optional
def wipe(name)
puts "Wiping #{name}..."
opts = {}
opts[:ignore] = true
opts[:all] = true
opts[:executables] = true
opts[:install_dir] = gem_dir if gem_dir
Merb.uninstall_gem(name, opts)
rescue => e
puts "Failed to wipe #{name} (#{e.message})"
end
# 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.
desc 'redeploy', 'Recreate any binary gems on the target deployment platform'
def redeploy
require 'tempfile' # for
if File.directory?(specs_dir = File.join(gem_dir, 'specifications')) &&
File.directory?(cache_dir = File.join(gem_dir, 'cache'))
Dir[File.join(specs_dir, '*.gemspec')].each do |gemspec_path|
unless (gemspec = Gem::Specification.load(gemspec_path)).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))
# 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.install_gem(gem_file_copy, :install_dir => gem_dir)
File.delete(gem_file_copy)
end
end
end
else
puts "No application local gems directory found"
end
end
end
class << self
# Default Git repositories - pass source_config option
# to load a yaml configuration file.
def repos(source_config = nil)
@_repos ||= begin
repositories = {
'merb-core' => "git://github.com/wycats/merb-core.git",
'merb-more' => "git://github.com/wycats/merb-more.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",
'thor' => "git://github.com/wycats/thor.git"
}
end
if source_config && File.exists?(source_config)
@_repos.merge(YAML.load(File.read(source_config)))
else
@_repos
end
end
# Install a gem - looks remotely and local gem cache;
# won't process rdoc or ri options.
def install_gem(gem, options = {})
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
installer = Gem::DependencyInstaller.new(options.merge(:user_install => false))
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
puts "Failed to install gem '#{gem}' (#{exception.message})"
end
installer.installed_gems.each do |spec|
puts "Successfully installed #{spec.full_name}"
end
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
puts "Failed to install gem '#{gem}' (#{e.message})"
end
installer.installed_gems.each do |spec|
puts "Successfully installed #{spec.full_name}"
end
end
# Install a gem from source - builds and packages it first then installs it.
def install_gem_from_src(gem_src_dir, options = {})
raise SourcePathMissing unless File.directory?(gem_src_dir)
raise GemPathMissing if options[:install_dir] && !File.directory?(options[:install_dir])
gem_name = File.basename(gem_src_dir)
gem_pkg_dir = File.expand_path(File.join(gem_src_dir, 'pkg'))
# We need to use local bin executables if available.
thor = which('thor')
rake = which('rake')
# Handle pure Thor installation instead of Rake
if File.exists?(File.join(gem_src_dir, 'Thorfile'))
# Remove any existing packages.
FileUtils.rm_rf(gem_pkg_dir) if File.directory?(gem_pkg_dir)
# Create the package.
FileUtils.cd(gem_src_dir) { system("#{thor} :package") }
# Install the package using rubygems.
if package = Dir[File.join(gem_pkg_dir, "#{gem_name}-*.gem")].last
FileUtils.cd(File.dirname(package)) do
install_gem(File.basename(package), options.dup)
return
end
else
raise Merb::GemInstallError, "No package found for #{gem_name}"
end
# Handle standard installation through Rake
else
# Clean and regenerate any subgems for meta gems.
Dir[File.join(gem_src_dir, '*', 'Rakefile')].each do |rakefile|
FileUtils.cd(File.dirname(rakefile)) { system("#{rake} clobber_package; #{rake} package") }
end
# Handle the main gem install.
if File.exists?(File.join(gem_src_dir, 'Rakefile'))
# Remove any existing packages.
FileUtils.cd(gem_src_dir) { system("#{rake} clobber_package") }
# Create the main gem pkg dir if it doesn't exist.
FileUtils.mkdir_p(gem_pkg_dir) unless File.directory?(gem_pkg_dir)
# Copy any subgems to the main gem pkg dir.
Dir[File.join(gem_src_dir, '**', 'pkg', '*.gem')].each do |subgem_pkg|
FileUtils.cp(subgem_pkg, gem_pkg_dir)
end
# Finally generate the main package and install it; subgems
# (dependencies) are local to the main package.
FileUtils.cd(gem_src_dir) do
system("#{rake} package")
if package = Dir[File.join(gem_pkg_dir, "#{gem_name}-*.gem")].last
FileUtils.cd(File.dirname(package)) do
install_gem(File.basename(package), options.dup)
return
end
else
raise Merb::GemInstallError, "No package found for #{gem_name}"
end
end
end
end
raise Merb::GemInstallError, "No Rakefile found for #{gem_name}"
end
# Uninstall a gem.
def uninstall_gem(gem, options = {})
if options[:version] && !options[:version].is_a?(Gem::Requirement)
options[:version] = Gem::Requirement.new ["= #{version}"]
end
begin
Gem::Uninstaller.new(gem, options).uninstall
rescue => e
raise GemUninstallError, "Failed to uninstall #{gem}"
end
end
# Will prepend sudo on a suitable platform.
def sudo
@_sudo ||= begin
windows = PLATFORM =~ /win32|cygwin/ rescue nil
windows ? "" : "sudo "
end
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
private
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
end
class Tasks < Thor
include MerbThorHelper
# Install Thor, Rake and RSpec into the local gems dir, by copying it from
# the system-wide rubygems cache - which is OK since we needed it to run
# this task already.
#
# After this we don't need the system-wide rubygems anymore, as all required
# executables are available in the local ./bin directory.
#
# RSpec is needed here because source installs might fail when running
# rake tasks where spec/rake/spectask has been required.
desc 'setup', 'Install Thor, Rake and RSpec in the local gems dir'
method_options "--merb-root" => :optional
def setup
if $0 =~ /^(\.\/)?bin\/thor$/
puts "You cannot run the setup from #{$0} - try #{File.basename($0)} merb:tasks:setup instead"
return
end
create_if_missing(File.join(working_dir, 'gems'))
Merb.install_gem('thor', :cache => true, :install_dir => gem_dir)
Merb.install_gem('rake', :cache => true, :install_dir => gem_dir)
Merb.install_gem('rspec', :cache => true, :install_dir => gem_dir)
ensure_local_bin_for('thor', 'rake', 'rspec')
end
end
end