Permalink
Browse files

Fix #54 - Add a dependency manager for managing platform specific gems.

  • Loading branch information...
Maher4Ever committed Jul 31, 2012
1 parent 38eec03 commit 2b7e3521629e75be13f703af89d66a8293921caf
View
@@ -3,13 +3,16 @@ source :rubygems
gemspec
gem 'rake'
-gem 'wdm', :github => 'Maher4Ever/wdm'
group :development do
platform :ruby do
gem 'coolline'
end
+ gem 'rb-fsevent', '~> 0.9.1' if RUBY_PLATFORM =~ /darwin(1.+)?$/i
+ gem 'rb-inotify', '~> 0.8.8' if RUBY_PLATFORM =~ /linux/i
+ gem 'wdm', '~> 0.0.2' if RUBY_PLATFORM =~ /mswin|mingw/i
+
gem 'guard'
gem 'guard-rspec'
gem 'yard'
@@ -21,4 +24,4 @@ end
group :test do
gem 'rspec'
-end
+end
View
@@ -251,7 +251,6 @@ want to force the use of the polling adapter, either use the `:force_polling` op
while initializing the listener or call the `force_polling` method on your listener
before starting it.
-<a name="fallback"/>
## Polling fallback
When a OS-specific adapter doesn't work the Listen gem automatically falls back to the polling adapter.
View
@@ -1,10 +1,11 @@
module Listen
- autoload :Turnstile, 'listen/turnstile'
- autoload :Listener, 'listen/listener'
- autoload :MultiListener, 'listen/multi_listener'
- autoload :DirectoryRecord, 'listen/directory_record'
- autoload :Adapter, 'listen/adapter'
+ autoload :Turnstile, 'listen/turnstile'
+ autoload :Listener, 'listen/listener'
+ autoload :MultiListener, 'listen/multi_listener'
+ autoload :DirectoryRecord, 'listen/directory_record'
+ autoload :DependencyManager, 'listen/dependency_manager'
+ autoload :Adapter, 'listen/adapter'
module Adapters
autoload :Darwin, 'listen/adapters/darwin'
View
@@ -10,8 +10,15 @@ class Adapter
# The default delay between checking for changes.
DEFAULT_LATENCY = 0.25
+ # The default warning message when there is a missing dependency.
+ MISSING_DEPENDENCY_MESSAGE = <<-EOS.gsub(/^\s*/, '')
+ For a better performance, it's recommended that you satisfy the missing dependency.
+ EOS
+
# The default warning message when falling back to polling adapter.
- POLLING_FALLBACK_MESSAGE = "WARNING: Listen has fallen back to polling, learn more at https://github.com/guard/listen#fallback."
+ POLLING_FALLBACK_MESSAGE = <<-EOS.gsub(/^\s*/, '')
+ Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.
+ EOS
# Selects the appropriate adapter implementation for the
# current OS and initializes it.
@@ -31,18 +38,26 @@ class Adapter
def self.select_and_initialize(directories, options = {}, &callback)
return Adapters::Polling.new(directories, options, &callback) if options.delete(:force_polling)
- if Adapters::Darwin.usable_and_works?(directories, options)
- Adapters::Darwin.new(directories, options, &callback)
- elsif Adapters::Linux.usable_and_works?(directories, options)
- Adapters::Linux.new(directories, options, &callback)
- elsif Adapters::Windows.usable_and_works?(directories, options)
- Adapters::Windows.new(directories, options, &callback)
- else
- unless options[:polling_fallback_message] == false
- Kernel.warn(options[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE)
+ warning = ''
+
+ begin
+ if Adapters::Darwin.usable_and_works?(directories, options)
+ return Adapters::Darwin.new(directories, options, &callback)
+ elsif Adapters::Linux.usable_and_works?(directories, options)
+ return Adapters::Linux.new(directories, options, &callback)
+ elsif Adapters::Windows.usable_and_works?(directories, options)
+ return Adapters::Windows.new(directories, options, &callback)
end
- Adapters::Polling.new(directories, options, &callback)
+ rescue DependencyManager::Error => e
+ warning += e.message + "\n" + MISSING_DEPENDENCY_MESSAGE
end
+
+ unless options[:polling_fallback_message] == false
+ warning += options[:polling_fallback_message] || POLLING_FALLBACK_MESSAGE
+ Kernel.warn "[Listen warning]:\n" + warning.gsub(/^(.*)/, ' \1')
+ end
+
+ Adapters::Polling.new(directories, options, &callback)
end
# Initializes the adapter.
@@ -116,6 +131,15 @@ def wait_for_changes(goal = 0)
end
end
+ # Checks if the adapter is usable on the current OS.
+ #
+ # @return [Boolean] whether usable or not
+ #
+ def self.usable?
+ load_depenencies
+ dependencies_loaded?
+ end
+
# Checks if the adapter is usable and works on the current OS.
#
# @param [String, Array<String>] directories the directories to watch
@@ -4,6 +4,10 @@ module Adapters
# Adapter implementation for Mac OS X `FSEvents`.
#
class Darwin < Adapter
+ extend DependencyManager
+
+ # Declare the adapter's dependencies
+ dependency 'rb-fsevent', '~> 0.9.1'
LAST_SEPARATOR_REGEX = /\/$/
@@ -54,11 +58,7 @@ def stop
#
def self.usable?
return false unless RbConfig::CONFIG['target_os'] =~ /darwin(1.+)?$/i
-
- require 'rb-fsevent'
- true
- rescue LoadError
- false
+ super
end
private
@@ -4,6 +4,10 @@ module Adapters
# Listener implementation for Linux `inotify`.
#
class Linux < Adapter
+ extend DependencyManager
+
+ # Declare the adapter's dependencies
+ dependency 'rb-inotify', '~> 0.8.8'
# Watched inotify events
#
@@ -59,17 +63,13 @@ def stop
@poll_thread.join if @poll_thread
end
- # Check if the adapter is usable on the current OS.
+ # Checks if the adapter is usable on the current OS.
#
# @return [Boolean] whether usable or not
#
def self.usable?
return false unless RbConfig::CONFIG['target_os'] =~ /linux/i
-
- require 'rb-inotify'
- true
- rescue LoadError
- false
+ super
end
private
@@ -10,6 +10,7 @@ module Adapters
# file IO that the other implementations.
#
class Polling < Adapter
+ extend DependencyManager
# Initialize the Adapter. See {Listen::Adapter#initialize} for more info.
#
@@ -6,6 +6,10 @@ module Adapters
# Adapter implementation for Windows `wdm`.
#
class Windows < Adapter
+ extend DependencyManager
+
+ # Declare the adapter's dependencies
+ dependency 'wdm', '~> 0.0.2'
# Initializes the Adapter. See {Listen::Adapter#initialize} for more info.
#
@@ -54,11 +58,7 @@ def stop
#
def self.usable?
return false unless RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
-
- require 'wdm'
- true
- rescue LoadError
- false
+ super
end
private
@@ -0,0 +1,126 @@
+require 'set'
+
+module Listen
+
+ # The dependency-manager offers a simple DSL which allows
+ # classes to declare their gem dependencies and load them when
+ # needed.
+ # It raises a user-friendly exception when the dependencies
+ # can't be loaded which has the install command in the message.
+ #
+ module DependencyManager
+
+ GEM_LOAD_MESSAGE = <<-EOS.gsub(/^ {6}/, '')
+ Missing dependency '%s' (version '%s')!
+ EOS
+
+ GEM_INSTALL_COMMAND = <<-EOS.gsub(/^ {6}/, '')
+ Please run the following to satisfy the dependency:
+ gem install --version '%s' %s
+ EOS
+
+ BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
+ Please add the following to your Gemfile to satisfy the dependency:
+ gem '%s', '%s'
+ EOS
+
+ Dependency = Struct.new(:name, :version)
+
+ # The error raised when a dependency can't be loaded.
+ class Error < StandardError; end
+
+ # A list of all loaded dependencies in the dependency manager.
+ @_loaded_dependencies = Set.new
+
+ # class methods
+ class << self
+
+ # Initializes the extended class.
+ #
+ # @param [Class] the class for which some dependencies must be managed
+ #
+ def extended(base)
+ base.class_eval do
+ @_dependencies = Set.new
+ end
+ end
+
+ # Adds a loaded dependency to a list so that it doesn't have
+ # to be loaded again by another classes.
+ #
+ # @param [Dependency] dependency
+ #
+ def add_loaded(dependency)
+ @_loaded_dependencies << dependency
+ end
+
+ # Returns whether the dependency is alread loaded or not.
+ #
+ # @param [Dependency] dependency
+ # @return [Boolean]
+ #
+ def already_loaded?(dependency)
+ @_loaded_dependencies.include?(dependency)
+ end
+
+ # Clears the list of loaded dependencies.
+ #
+ def clear_loaded
+ @_loaded_dependencies.clear
+ end
+ end
+
+ # Registers a new dependency.
+ #
+ # @param [String] name the name of the gem
+ # @param [String] version the version of the gem
+ #
+ def dependency(name, version)
+ @_dependencies << Dependency.new(name, version)
+ end
+
+ # Loads the registered dependencies.
+ #
+ # @raise DependencyManager::Error if the dependency can't be loaded.
+ #
+ def load_depenencies
+ @_dependencies.each do |dependency|
+ begin
+ next if DependencyManager.already_loaded?(dependency)
+ gem(dependency.name, dependency.version)
+ require(dependency.name)
+ DependencyManager.add_loaded(dependency)
+ @_dependencies.delete(dependency)
+ rescue Gem::LoadError
+ args = [dependency.name, dependency.version]
+ command = if running_under_bundler?
+ BUNDLER_DECLARE_GEM % args
+ else
+ GEM_INSTALL_COMMAND % args.reverse
+ end
+ message = GEM_LOAD_MESSAGE % args
+
+ raise Error.new(message + command)
+ end
+ end
+ end
+
+ # Returns whether all the dependencies has been loaded or not.
+ #
+ # @return [Boolean]
+ #
+ def dependencies_loaded?
+ @_dependencies.empty?
+ end
+
+ private
+
+ # Returns whether we are running under bundler or not
+ #
+ # @return [Boolean]
+ #
+ def running_under_bundler?
+ !!(File.exists?('Gemfile') && ENV['BUNDLE_GEMFILE'])
+ end
+ end
+end
View
@@ -15,9 +15,6 @@ Gem::Specification.new do |s|
s.required_rubygems_version = '>= 1.3.6'
s.rubyforge_project = 'listen'
- s.add_dependency 'rb-fsevent', '~> 0.9.1'
- s.add_dependency 'rb-inotify', '~> 0.8.8'
-
s.add_development_dependency 'bundler'
s.files = Dir.glob('{lib}/**/*') + %w[CHANGELOG.md LICENSE README.md]
@@ -31,14 +31,27 @@
described_class.select_and_initialize('dir')
end
- it "warns with the default polling fallback message" do
- Kernel.should_receive(:warn).with(Listen::Adapter::POLLING_FALLBACK_MESSAGE)
+ it 'warns with the default polling fallback message' do
+ Kernel.should_receive(:warn).with(/#{Listen::Adapter::POLLING_FALLBACK_MESSAGE}/)
described_class.select_and_initialize('dir')
end
+ context 'when the dependencies of an adapter are not satisfied' do
+ before do
+ Listen::Adapters::Darwin.stub(:usable_and_works?).and_raise(Listen::DependencyManager::Error)
+ Listen::Adapters::Linux.stub(:usable_and_works?).and_raise(Listen::DependencyManager::Error)
+ Listen::Adapters::Windows.stub(:usable_and_works?).and_raise(Listen::DependencyManager::Error)
+ end
+
+ it 'invites the user to satisfy the dependencies of the adapter in the warning' do
+ Kernel.should_receive(:warn).with(/#{Listen::Adapter::MISSING_DEPENDENCY_MESSAGE}/)
+ described_class.select_and_initialize('dir')
+ end
+ end
+
context "with custom polling_fallback_message option" do
it "warns with the custom polling fallback message" do
- Kernel.should_receive(:warn).with('custom')
+ Kernel.should_receive(:warn).with(/custom/)
described_class.select_and_initialize('dir', :polling_fallback_message => 'custom')
end
end
@@ -102,6 +115,14 @@
[Listen::Adapters::Darwin, Listen::Adapters::Linux, Listen::Adapters::Windows].each do |adapter_class|
if adapter_class.usable?
+ describe '.usable?' do
+ it 'checks the dependencies' do
+ adapter_class.should_receive(:load_depenencies)
+ adapter_class.should_receive(:dependencies_loaded?)
+ adapter_class.usable?
+ end
+ end
+
describe '.usable_and_works?' do
it 'checks if the adapter is usable' do
adapter_class.stub(:works?)
Oops, something went wrong.

2 comments on commit 2b7e352

@prusswan

This comment has been minimized.

Show comment Hide comment
@prusswan

prusswan Sep 25, 2012

Hi, how is the new dependency manager supposed to work with guard? I just realized that on Ubuntu, the rb-inotify, rb-fchange, rb-fsevent gems are no longer included, and a warning will be shown:

> [Listen warning]:
  Missing dependency 'rb-inotify' (version '~> 0.8.8')!
  Please add the following to your Gemfile to satisfy the dependency:
    gem 'rb-inotify', '~> 0.8.8'

  For a better performance, it's recommended that you satisfy the missing dependency.
  Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.

Note that this is not immediately noticeable for those who updated from older versions of guard, since those gems may still be present in Gemfile.lock

Hi, how is the new dependency manager supposed to work with guard? I just realized that on Ubuntu, the rb-inotify, rb-fchange, rb-fsevent gems are no longer included, and a warning will be shown:

> [Listen warning]:
  Missing dependency 'rb-inotify' (version '~> 0.8.8')!
  Please add the following to your Gemfile to satisfy the dependency:
    gem 'rb-inotify', '~> 0.8.8'

  For a better performance, it's recommended that you satisfy the missing dependency.
  Listen will be polling changes. Learn more at https://github.com/guard/listen#polling-fallback.

Note that this is not immediately noticeable for those who updated from older versions of guard, since those gems may still be present in Gemfile.lock

@thibaudgg

This comment has been minimized.

Show comment Hide comment
@thibaudgg

thibaudgg Sep 25, 2012

Member

Just add

gem 'rb-inotify', '~> 0.8.8'

before gem 'guard' and you'll be good to go.

Member

thibaudgg replied Sep 25, 2012

Just add

gem 'rb-inotify', '~> 0.8.8'

before gem 'guard' and you'll be good to go.

Please sign in to comment.