Skip to content

Commit

Permalink
Fix #54 - Add a dependency manager for managing platform specific gems.
Browse files Browse the repository at this point in the history
  • Loading branch information
Maher4Ever committed Jul 31, 2012
1 parent 38eec03 commit 2b7e352
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 41 deletions.
7 changes: 5 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,4 +24,4 @@ end

group :test do
gem 'rspec'
end
end
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions lib/listen.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
46 changes: 35 additions & 11 deletions lib/listen/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions lib/listen/adapters/darwin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /\/$/

Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions lib/listen/adapters/linux.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/listen/adapters/polling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down
10 changes: 5 additions & 5 deletions lib/listen/adapters/windows.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
#
Expand Down Expand Up @@ -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
Expand Down
126 changes: 126 additions & 0 deletions lib/listen/dependency_manager.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 0 additions & 3 deletions listen.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
27 changes: 24 additions & 3 deletions spec/listen/adapter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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?)
Expand Down
Loading

2 comments on commit 2b7e352

@prusswan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just add

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

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

Please sign in to comment.