From 0a735ca4b3bd28dc7760b3db816d13ddc6db56a5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 28 Oct 2005 21:21:07 +0000 Subject: Added script/plugin to manage plugins (install, remove, list, etc) [Ryan Tomayko] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2793 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- railties/lib/commands/plugin.rb | 745 +++++++++++++++++++++ .../generators/applications/app/app_generator.rb | 2 +- 2 files changed, 746 insertions(+), 1 deletion(-) create mode 100644 railties/lib/commands/plugin.rb (limited to 'railties/lib') diff --git a/railties/lib/commands/plugin.rb b/railties/lib/commands/plugin.rb new file mode 100644 index 0000000000..3089c56797 --- /dev/null +++ b/railties/lib/commands/plugin.rb @@ -0,0 +1,745 @@ +#!/usr/local/bin/ruby + +# Rails Plugin Manager. +# +# Listing available plugins: +# +# $ ./script/plugin list +# continuous_builder http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder +# asset_timestamping http://svn.aviditybytes.com/rails/plugins/asset_timestamping +# enumerations_mixin http://svn.protocool.com/rails/plugins/enumerations_mixin/trunk +# calculations http://techno-weenie.net/svn/projects/calculations/ +# ... +# +# Installing plugins: +# +# $ ./script/plugin install continuous_builder asset_timestamping +# +# Finding Repositories: +# +# $ ./script/plugin discover +# +# Adding Repositories: +# +# $ ./script/plugin source http://svn.protocool.com/rails/plugins/ +# +# How it works: +# +# * Maintains a list of subversion repositories that are assumed to have +# a plugin directory structure. Manage them with the (source, unsource, +# and sources commands) +# +# * The discover command scrapes the following page for things that +# look like subversion repositories with plugins: +# http://wiki.rubyonrails.org/rails/pages/Plugins +# +# * If `vendor/plugins` is under subversion control, the script will +# modify the svn:externals property and perform an update. You can +# use normal subversion commands to keep the plugins up to date or +# you can use the built in update command. +# +# * Or, if `vendor/plugins` is not under subversion control, the +# plugin is pulled via `svn checkout` or `svn export` but looks +# exactly the same. +# +# This is Free Software, copyright 2005 by Ryan Tomayko and is licensed +# MIT: (http://www.opensource.org/licenses/mit-license.php) +# +# Send patches to rtomayko@gmail.com + +$verbose = false + +require 'fileutils' +require 'tempfile' + +include FileUtils + +class RailsEnvironment + attr_reader :root + + def initialize(dir) + @root = dir + end + + def self.find(dir=nil) + dir ||= pwd + while dir.length > 1 + return new(dir) if File.exist?(File.join(dir, 'config', 'environment.rb')) + dir = File.dirname(dir) + end + end + + def self.default + @default ||= find + end + + def self.default=(rails_env) + @default = rails_env + end + + def install(name_uri_or_plugin) + if name_uri_or_plugin.is_a? String + if name_uri_or_plugin =~ /:\/\// + plugin = Plugin.new(name_uri_or_plugin) + else + plugin = Plugins[name_uri_or_plugin] + end + else + plugin = name_uri_or_plugin + end + unless plugin.nil? + plugin.install + else + puts "plugin not found: #{name_uri_or_plugin}" + end + end + + def use_externals? + File.directory?("#{root}/vendor/plugins/.svn") + end + + def use_checkout? + # this is a bit of a guess. we assume that if the rails environment + # is under subversion than they probably want the plugin checked out + # instead of exported. This can be overridden on the command line + File.directory?("#{root}/.svn") + end + + def best_install_method + case + when use_externals? then :externals + when use_checkout? then :checkout + else :export + end + end + + def externals + return [] unless use_externals? + ext = `svn propget svn:externals #{root}/vendor/plugins 2> /dev/null` + ext.reject{ |line| line.strip == '' }.map do |line| + line.strip.split(/\s+/, 2) + end + end + + def externals=(items) + unless items.is_a? String + items = items.map{|name,uri| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n") + end + Tempfile.open("svn-set-prop") do |file| + file.write(items) + file.flush + system("svn propset svn:externals -F #{file.path} #{root}/vendor/plugins") + end + end + +end + +class Plugin + attr_reader :name, :uri + + def initialize(uri, name=nil) + @uri = uri + guess_name(uri) + end + + def to_s + "#{@name.ljust(30)}#{@uri}" + end + + def installed? + File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \ + or rails_env.externals.detect{ |name, repo| self.uri == repo } + end + + def install(method=nil) + method ||= rails_env.best_install_method? + unless installed? + send("install_using_#{method}") + else + puts "already installed: #{name} (#{uri})" + end + end + + private + def install_using_export + root = rails_env.root + mkdir_p "#{root}/vendor/plugins" + system("svn export #{uri} #{root}/vendor/plugins/#{name}") + end + + def install_using_checkout + root = rails_env.root + mkdir_p "#{root}/vendor/plugins" + system("svn checkout #{uri} #{root}/vendor/plugins/#{name}") + end + + def install_using_externals + externals = rails_env.externals + externals.push([@name, uri]) + rails_env.externals = externals + install_using_checkout + end + + def guess_name(url) + @name = File.basename(url) + if @name == 'trunk' || @name.empty? + @name = File.basename(File.dirname(url)) + end + end + + def rails_env + @rails_env || RailsEnvironment.default + end +end + +class Repositories + include Enumerable + + def initialize(cache_file="~/.rails-plugin-sources") + @cache_file = File.expand_path(cache_file) + load! + end + + def each(&block) + @repositories.each(&block) + end + + def add(uri) + unless find{|repo| repo.uri == uri } + @repositories.push(Repository.new(uri)).last + end + end + + def remove(uri) + @repositories.reject!{|repo| repo.uri == uri} + end + + def exist?(uri) + @repositories.detect{|repo| repo.uri == uri } + end + + def all + @repositories + end + + def find_plugin(name) + @repositories.each do |repo| + repo.each do |plugin| + return plugin if plugin.name == name + end + end + end + + def load! + contents = File.exist?(@cache_file) ? File.read(@cache_file) : defaults + contents = defaults if contents.empty? + @repositories = contents.split(/\n/).reject do |line| + line =~ /^\s*#/ or line =~ /^\s*$/ + end.map { |source| Repository.new(source.strip) } + end + + def save + File.open(@cache_file, 'w') do |f| + each do |repo| + f.write(repo.uri) + f.write("\n") + end + end + end + + def defaults + <<-REPOSITORIES + http://dev.rubyonrails.org/svn/rails/plugins/ + http://lesscode.org/svn/rtomayko/rails/plugins/ + http://svn.aviditybytes.com/rails/plugins/ + REPOSITORIES + end + + def self.instance + @instance ||= Repositories.new + end + + def self.each(&block) + self.instance.each(&block) + end +end + +class Repository + include Enumerable + attr_reader :uri, :plugins + + def initialize(uri) + uri << "/" unless uri =~ /\/$/ + @uri = uri + @plugins = nil + end + + def plugins + unless @plugins + if $verbose + puts "Discovering plugins in #{@uri}" + puts index + end + + @plugins = index.split(/\n/).reject{ |line| line !~ /\/$/ } + @plugins.map! { |name| Plugin.new(File.join(@uri, name), name) } + end + + @plugins + end + + def each(&block) + plugins.each(&block) + end + + private + def index + @index ||= `svn ls #{@uri}` + end +end + + +# load default environment and parse arguments +require 'optparse' +module Commands + + class Plugin + attr_reader :environment, :script_name, :sources + def initialize + @environment = RailsEnvironment.default + @rails_root = RailsEnvironment.default.root + @script_name = File.basename($0) + @sources = [] + end + + def environment=(value) + @environment = value + RailsEnvironment.default = value + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@script_name} [OPTIONS] command" + o.define_head "Rails plugin manager." + + o.separator "" + o.separator "GENERAL OPTIONS" + + o.on("-r", "--root=DIR", String, + "Set an explicit rails app directory.", + "Default: #{@rails_root}") { |@rails_root| self.environment = RailsEnvironment.new(@rails_root) } + o.on("-s", "--source=URL1,URL2", Array, + "Use the specified plugin repositories instead of the defaults.") { |@sources|} + + o.on("-v", "--verbose", "Turn on verbose output.") { |$verbose| } + o.on("-h", "--help", "Show this help message.") { puts o; exit } + + o.separator "" + o.separator "COMMANDS" + + o.separator " discover Discover plugin repositories." + o.separator " list List available plugins." + o.separator " install Install plugin(s) from known repositories or URLs." + o.separator " update Update installed plugins." + o.separator " remove Uninstall plugins." + o.separator " source Add a plugin source repository." + o.separator " unsource Remove a plugin repository." + o.separator " sources List currently configured plugin repositories." + + o.separator "" + o.separator "EXAMPLES" + o.separator " Install a plugin:" + o.separator " #{@script_name} install continuous_builder\n" + o.separator " Install a plugin from a subversion URL:" + o.separator " #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n" + o.separator " Install a plugin from a subversion URL:" + o.separator " #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n" + o.separator " List all available plugins:" + o.separator " #{@script_name} list\n" + o.separator " List plugins in the specified repository:" + o.separator " #{@script_name} list --source=http://dev.rubyonrails.com/svn/rails/plugins/\n" + o.separator " Discover and prompt to add new repositories:" + o.separator " #{@script_name} discover\n" + o.separator " Discover new repositories but just list them, don't add anything:" + o.separator " #{@script_name} discover -l\n" + o.separator " Add a new repository to the source list:" + o.separator " #{@script_name} source http://dev.rubyonrails.com/svn/rails/plugins/\n" + end + end + + def parse!(args=ARGV) + general, sub = split_args(args) + options.parse!(general) + + command = general.shift + if command =~ /^(list|discover|install|source|unsource|sources|remove|update)$/ + command = Commands.const_get(command.capitalize).new(self) + command.parse!(sub) + else + puts "Unknown command: #{command}" + puts "Try: #{$0} --help" + exit 1 + end + end + + def split_args(args) + left = [] + left << args.shift while args[0] and args[0] =~ /^-/ + left << args.shift if args[0] + return [left, args] + end + + def self.parse!(args=ARGV) + Plugin.new.parse!(args) + end + end + + + class List + def initialize(base_command) + @base_command = base_command + @sources = [] + @local = false + @remote = true + @details = false + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} list [OPTIONS] [PATTERN]" + o.define_head "List available plugins." + o.separator "" + o.separator "Options:" + o.separator "" + o.on( "-s", "--source=URL1,URL2", Array, + "Use the specified plugin repositories.") {|@sources|} + o.on( "--local", + "List locally installed plugins.") {|@local| @remote = false} + o.on( "--remote", + "List remotely availabled plugins. This is the default behavior", + "unless --local is provided.") {|@remote|} + o.on( "-l", "--long", + "Long listing / details about each plugin.") {|@details|} + end + end + + def parse!(args) + options.order!(args) + unless @sources.empty? + @sources.map!{ |uri| Repository.new(uri) } + else + @sources = Repositories.instance.all + end + if @remote + @sources.map{|r| r.plugins}.flatten.each do |plugin| + if @local or !plugin.installed? + puts plugin.to_s + system "svn info #{plugin.uri}" if @details + end + end + else + cd "#{@base_command.environment.root}/vendor/plugins" + Dir["*"].select{|p| File.directory?(p)}.each do |name| + puts name + system "svn info #{name}" if @details + end + end + end + end + + + class Sources + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} sources [OPTIONS] [PATTERN]" + o.define_head "List configured plugin repositories." + o.separator "" + o.separator "Options:" + o.separator "" + o.on( "-c", "--check", + "Report status of repository.") { |@sources|} + end + end + + def parse!(args) + options.parse!(args) + Repositories.each do |repo| + puts repo.uri + end + end + end + + + class Source + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} source REPOSITORY" + o.define_head "Add a new repository." + end + end + + def parse!(args) + options.parse!(args) + count = 0 + args.each do |uri| + if Repositories.instance.add(uri) + puts "added: #{uri.ljust(50)}" if $verbose + count += 1 + else + puts "failed: #{uri.ljust(50)}" + end + end + Repositories.instance.save + puts "Added #{count} repositories." + end + end + + + class Unsource + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} source URI [URI [URI]...]" + o.define_head "Remove repositories from the default search list." + o.separator "" + o.on_tail("-h", "--help", "Show this help message.") { puts o; exit } + end + end + + def parse!(args) + options.parse!(args) + count = 0 + args.each do |uri| + if Repositories.instance.remove(uri) + count += 1 + puts "removed: #{uri.ljust(50)}" + else + puts "failed: #{uri.ljust(50)}" + end + end + Repositories.instance.save + puts "Removed #{count} repositories." + end + end + + + class Discover + def initialize(base_command) + @base_command = base_command + @list = false + @prompt = true + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} discover URI [URI [URI]...]" + o.define_head "Discover repositories referenced on a page." + o.separator "" + o.separator "Options:" + o.separator "" + o.on( "-l", "--list", + "List but don't prompt or add discovered repositories.") { |@list| @prompt = !@list } + o.on( "-n", "--no-prompt", + "Add all new repositories without prompting.") { |v| @prompt = !v } + end + end + + def parse!(args) + options.parse!(args) + args = ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args.empty? + args.each do |uri| + scrape(uri) do |repo_uri| + catch(:next_uri) do + if @prompt + begin + $stdout.print "Add #{repo_uri}? [Y/n] " + throw :next_uri if $stdin.gets !~ /^y?$/i + rescue Interrupt + $stdout.puts + exit 1 + end + elsif @list + puts repo_uri + throw :next_uri + end + Repositories.instance.add(repo_uri) + puts "discovered: #{repo_uri}" if $verbose or !@prompt + end + end + end + Repositories.instance.save + end + + def scrape(uri) + require 'open-uri' + puts "Scraping #{uri}" if $verbose + dupes = [] + content = open(uri).each do |line| + if line =~ /]*href=['"]([^'"]*)['"]/ + uri = $1 + if uri =~ /\/plugins\// and uri !~ /\/browser\// + uri = extract_repository_uri(uri) + yield uri unless dupes.include?(uri) or Repositories.instance.exist?(uri) + dupes << uri + end + end + end + end + + def extract_repository_uri(uri) + uri.match(/(svn|https?):.*\/plugins\//i)[0] + end + end + + class Install + def initialize(base_command) + @base_command = base_command + @externals = false + @checkout = false + @export = false + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} install PLUGIN [PLUGIN [PLUGIN] ...]" + o.define_head "Install one or more plugins." + o.separator "" + o.separator "Options:" + o.on( "-e", "--externals", + "Use svn:externals. This is the default behavior when", + "vendor/plugins is under subversion control.") { |@externals| } + o.on( "-o", "--checkout", + "Use checkout. This is turns externals off and is the default", + "behavior when vendor/plugins is not under subversion control.") { |@checkout| } + o.on( "-x", "--export", + "Use export. This turns externals support off and is the default", + "behavior when the current project is not under subversion control.") { |@export| } + o.separator "" + o.separator "You can specify plugin names as given in 'plugin list' output or absolute URLs to " + o.separator "a plugin repository." + end + end + + def determine_install_method + requested = case + when @export then method = :export + when @checkout then method = :checkout + else method = :externals + end + best = @base_command.environment.best_install_method + if best == :export and requested != :export + puts "Cannot install using #{requested} because this project is not under subversion." + exit 1 + end + if best == :checkout and @externals + puts "Cannot install using externals because vendor/plugins is not under subversion." + exit 1 + end + requested + end + + def parse!(args) + options.parse!(args) + environment = @base_command.environment + install_method = determine_install_method + puts "Plugins will be installed using #{install_method}" if $verbose + args.each do |name| + if name =~ /\// then + Plugin.new(name).install(install_method) + else + plugin = Repositories.instance.find_plugin(name) + unless plugin.nil? + plugin.install(install_method) + else + puts "Plugin not found: #{name}" + exit 1 + end + end + end + end + end + + + class Remove + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} remove name [name]..." + o.define_head "Remove plugins." + end + end + + def parse!(args) + options.parse!(args) + root = @base_command.environment.root + args.each do |name| + path = "#{root}/vendor/plugins/#{name}" + if File.directory?(path) + rm_r path + else + puts "Plugin doesn't exist: #{path}" + end + # clean up svn:externals + externals = @base_command.environment.externals + externals.reject!{|n,u| name == n or name == u} + @base_command.environment.externals = externals + end + end + end + + class Update + def initialize(base_command) + @base_command = base_command + end + + def options + OptionParser.new do |o| + o.set_summary_indent(' ') + o.banner = "Usage: #{@base_command.script_name} update [name [name]...]" + o.define_head "Update plugins." + end + end + + def parse!(args) + options.parse!(args) + root = @base_command.environment.root + args = Dir["#{root}/vendor/plugins/*"].map do |f| + File.directory?("#{f}/.svn") ? File.basename(f) : nil + end.compact if args.empty? + cd "#{root}/vendor/plugins" + args.each do |name| + if File.directory?(name) + puts "Updating plugin: #{name}" + system("svn #{$verbose ? '' : '-q'} up #{name}") + else + puts "Plugin doesn't exist: #{name}" + end + end + end + end + +end + +Commands::Plugin.parse! \ No newline at end of file diff --git a/railties/lib/rails_generator/generators/applications/app/app_generator.rb b/railties/lib/rails_generator/generators/applications/app/app_generator.rb index 65d6afd9ab..71e0382723 100644 --- a/railties/lib/rails_generator/generators/applications/app/app_generator.rb +++ b/railties/lib/rails_generator/generators/applications/app/app_generator.rb @@ -49,7 +49,7 @@ class AppGenerator < Rails::Generator::Base m.file "environments/test.rb", "config/environments/test.rb" # Scripts - %w( breakpointer console destroy generate performance/benchmarker performance/profiler process/reaper process/spawner process/spinner runner server lighttpd ).each do |file| + %w( breakpointer console destroy generate performance/benchmarker performance/profiler process/reaper process/spawner process/spinner runner server lighttpd plugin ).each do |file| m.file "bin/#{file}", "script/#{file}", script_options end -- cgit v1.2.3