zactor 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,17 +1,13 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  group :development do
4
- gem "rspec", ">= 2"
4
+ gem "rspec", "~> 2.3.0"
5
5
  gem "yard", "~> 0.6.0"
6
6
  gem "bundler", "~> 1.0.0"
7
+ gem "jeweler", "~> 1.5.2"
7
8
  gem "rcov", ">= 0"
8
9
  gem "bluecloth"
9
10
  gem "rr"
10
- gem "guard-rspec"
11
- gem "growl"
12
- gem "ruby-debug19"
13
- gem "em-spec", :git => "https://github.com/mloughran/em-spec.git", :branch => 'rspec2'
14
11
  end
15
12
 
16
-
17
13
  gemspec
data/Gemfile.lock CHANGED
@@ -1,80 +1,33 @@
1
- GIT
2
- remote: https://github.com/mloughran/em-spec.git
3
- revision: fc888b289a424fa93848ffdac3dc4b24fdd34950
4
- branch: rspec2
5
- specs:
6
- em-spec (0.2.2)
7
-
8
1
  PATH
9
2
  remote: .
10
3
  specs:
11
- zactor (0.0.4)
4
+ zactor (0.0.5)
12
5
  activesupport (> 0.1)
13
- bson (> 0.1)
14
- bson_ext (> 0.1)
15
- em-zeromq (> 0.1)
16
- ffi (> 0.1)
17
- ffi-rzmq (> 0.1)
18
- ruby-interface (> 0)
6
+ i18n (> 0.1)
19
7
 
20
8
  GEM
21
9
  remote: http://rubygems.org/
22
10
  specs:
23
11
  activesupport (3.0.7)
24
- archive-tar-minitar (0.5.2)
25
12
  bluecloth (2.1.0)
26
- bson (1.2.4)
27
- bson_ext (1.2.4)
28
- columnize (0.3.2)
29
- configuration (1.2.0)
30
13
  diff-lcs (1.1.2)
31
- em-zeromq (0.2.1)
32
- eventmachine (>= 1.0.0.beta.3)
33
- ffi-rzmq (>= 0.7.2)
34
- eventmachine (1.0.0.beta.3)
35
- ffi (1.0.7)
36
- rake (>= 0.8.7)
37
- ffi-rzmq (0.7.2)
38
- growl (1.0.3)
39
- guard (0.3.0)
40
- open_gem (~> 1.4.2)
41
- thor (~> 0.14.6)
42
- guard-rspec (0.2.0)
43
- guard (>= 0.2.2)
14
+ git (1.2.5)
44
15
  i18n (0.5.0)
45
- launchy (0.3.7)
46
- configuration (>= 0.0.5)
47
- rake (>= 0.8.1)
48
- linecache19 (0.5.11)
49
- ruby_core_source (>= 0.1.4)
50
- open_gem (1.4.2)
51
- launchy (~> 0.3.5)
16
+ jeweler (1.5.2)
17
+ bundler (~> 1.0.0)
18
+ git (>= 1.2.5)
19
+ rake
52
20
  rake (0.8.7)
53
21
  rcov (0.9.9)
54
22
  rr (1.0.2)
55
- rspec (2.5.0)
56
- rspec-core (~> 2.5.0)
57
- rspec-expectations (~> 2.5.0)
58
- rspec-mocks (~> 2.5.0)
59
- rspec-core (2.5.1)
60
- rspec-expectations (2.5.0)
23
+ rspec (2.3.0)
24
+ rspec-core (~> 2.3.0)
25
+ rspec-expectations (~> 2.3.0)
26
+ rspec-mocks (~> 2.3.0)
27
+ rspec-core (2.3.1)
28
+ rspec-expectations (2.3.0)
61
29
  diff-lcs (~> 1.1.2)
62
- rspec-mocks (2.5.0)
63
- ruby-debug-base19 (0.11.24)
64
- columnize (>= 0.3.1)
65
- linecache19 (>= 0.5.11)
66
- ruby_core_source (>= 0.1.4)
67
- ruby-debug19 (0.11.6)
68
- columnize (>= 0.3.1)
69
- linecache19 (>= 0.5.11)
70
- ruby-debug-base19 (>= 0.11.19)
71
- ruby-interface (0.0.2)
72
- activesupport (> 0.1)
73
- i18n (> 0.1)
74
- ruby-interface
75
- ruby_core_source (0.1.4)
76
- archive-tar-minitar (>= 0.5.2)
77
- thor (0.14.6)
30
+ rspec-mocks (2.3.0)
78
31
  yard (0.6.5)
79
32
 
80
33
  PLATFORMS
@@ -83,12 +36,9 @@ PLATFORMS
83
36
  DEPENDENCIES
84
37
  bluecloth
85
38
  bundler (~> 1.0.0)
86
- em-spec!
87
- growl
88
- guard-rspec
39
+ jeweler (~> 1.5.2)
89
40
  rcov
90
41
  rr
91
- rspec (>= 2)
92
- ruby-debug19
42
+ rspec (~> 2.3.0)
93
43
  yard (~> 0.6.0)
94
44
  zactor!
data/README.md CHANGED
@@ -1,167 +1,93 @@
1
- Что это
2
- =======
3
- Zactor позволяет любой руби-объект научить общаться с другими объектами посредством отправки и получения сообщений. При этом неважно где именно расположен объект, в том же процессе, в соседнем, или на другой физической машине.
1
+ RubyInterface
2
+ =============
4
3
 
5
- Zactor использует zmq как бекенд для сообщений, eventmachine и em-zeromq для асинхронной работы с zmq, RubyInterface для описания себя, BSON для сериализации данных.
4
+ Простенький патерн определения интерфейсов в руби. В противовес стандартным миксинам, для каждого интерфейса создается свой класс и соответсвенно экземпляр класса для каждого объекта с интерфейсом.
6
5
 
7
- Каждый zactor-объект имеет свой identity, который генерируется автоматически, либо задается вручную и постоянен. Совокупность identity, host и port на которых был рожден этот объект, это достаточная информация для того что бы отправить этому объекту сообщение из другого объекта. Выглядит примерно так:
8
-
9
- zactor.actor =>
10
- {"identity"=>"actor.2154247120-0.0.0.0:8000", "host"=>"0.0.0.0:8000"}
11
-
12
- Ипользование
13
- ============
14
-
15
- Для начала нужно стартануть Zactor.
16
-
17
- Zactor.start 8000
18
-
19
- Процесс забиндится на 0.0.0.0:8000, через эту точку будет происходить общение между zactor-процессами. Стартоваться должен в запущенном EM-контексте.
20
-
21
- В каждый zactor-активный класс нужно делать include Zactor, после чего у класса и его экземпляров для доступа к функциями Zactor появится метод zactor. После создания объекта нужно выполнить zactor.init
22
-
23
- class A
24
- include Zactor
25
-
26
- def initialize
27
- zactor.init
28
- end
29
- end
30
-
31
- Для отправки сообщений другому объекту нам нужно знать его идентификатор. Идентификатор можно получить тремя способами:
32
-
33
- * Непосрдественной передачей. При инициализации или в любом другом месте, это исключительно внутренняя логика приложения. Идентификатор объекта можно получить вызвав zactor.actor
34
- * При получении сообщения. В сообщении всегда содержится информация об отправителе
35
- * Если объект имеет заранее известный identity, то мы можем получить его полный идентификатор вызвав Zactor.get_actor с identity и хостом, на котором он запущен
36
-
37
- actor = Zactor.get_actor "broker", :host => "0.0.0.0:8001"
38
-
39
- Получив идентификатор можно отправлять ему сообщения
40
-
41
- zactor.send_request actor, :show_me, :boobs
42
-
43
-
44
- Каждый класс может определять какие именно ивенты он может получать и что с ними делать
45
-
46
- include Zactor
47
-
48
- zactor do
49
- event(:show_me) do |o, msg, what|
50
- case what
51
- when :boobs
52
- do_it
53
- else
54
- do_smth_else
6
+ module Tree
7
+ extend RubyInterface
8
+ interface :tree do
9
+ include Enumerable
10
+ attr_accessor :parent
11
+
12
+ def childs
13
+ @childs ||= []
55
14
  end
56
- end
57
- end
58
-
59
- Рассмотрим пример банального ping-pong
60
-
61
- class A
62
- include Zactor
63
-
64
- def initialize
65
- zactor.init
66
- ping Zactor.get_actor("b")
67
- end
68
-
69
- def ping(actor)
70
- puts "Ping!"
71
- zactor.send_request actor, :ping do |res|
72
- puts res
15
+
16
+ def each(&blk)
17
+ blk.call(owner)
18
+ childs.each { |v| v.tree.each(&blk) }
73
19
  end
74
- end
75
- end
76
-
77
-
78
- class B
79
- include Zactor
80
-
81
- zactor do
82
- identity "b"
83
20
 
84
- event(:ping) do |o, msg|
85
- msg.reply "Pong!"
21
+ def set_parent(parent)
22
+ parent.tree.childs << owner
23
+ @parent = parent
86
24
  end
87
25
  end
88
-
89
- def initialize
90
- zactor.init
91
- end
92
-
93
26
  end
94
27
 
95
- EM.run do
96
- Zactor.start 8000
97
-
98
- a = A.new
99
- b = B.new
28
+ class A
29
+ include Tree
100
30
  end
101
-
102
- A посылает сообщение :ping для B, а B отвечает "Pong!"
103
-
104
- В коллбэк определенный в event передается объект получившый сообщение, объект сообщения ({Zactor::Message}) и далее переданные в запросе аргументы (если они есть). У {Zactor::Message} есть два основных метода: sender, возвращающий идентификатор отправителя и reply, который посылает ответ на запрос.
105
-
106
- Важный момент, identity должно задаваться ДО zactor.init и после этого не может меняться.
107
-
108
- ZMQ
109
- ===
110
-
111
- При Zactor.start стартует брокер, по одному на каждый процесс, через него проходят все сообщения данного процесса, принимает сообщения через SUB-сокет, отправляет через PUB. SUB подписан на все сообщения. Каждый zactor-объект создает по паре сокетов, PUB подключается к SUB-брокера, а SUB к PUB-брокера. SUB подписывается на сообщения содержащие его identity.
112
-
113
- ![ZMQ](images/zmq1.png)
114
-
115
- Рассмотрим жизнь сообщения на примере с ping-ping. В случае с b в том же процессе:
116
-
117
- <div class=wsd wsd_style="default"><pre>
118
- A[PUB]->Broker[SUB]: Посылаем запрос :ping
119
- Broker[SUB]->Broker[PUB]: Перебрасываем запрос в PUB сокет
120
- Broker[PUB]->B[SUB]: Передаем получателю сообщение
121
- B[PUB]->Broker[SUB]: Отправляем ответ "Pong!"
122
- Broker[SUB]->Broker[PUB]: Перебрасываем запрос в PUB сокет
123
- Broker[PUB]->A[SUB]: Отправитель получает ответ
124
- </pre></div><script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
125
31
 
126
- В случае с b в другом процессе:
32
+ При разработке интерфейса не нужно задумываться о конфликтах имен переменных, методов, можно делать все что угодно. Аргументом к методу interface передается название метода, по которому этот интерфейс будет доступен.
127
33
 
128
- <div class=wsd wsd_style="default"><pre>
129
- A[PUB for App2]->App2 Broker[SUB]: Посылаем запрос :ping
130
- App2 Broker[SUB]->App2 Broker[PUB]: Перебрасываем запрос в PUB сокет
131
- App2 Broker[PUB]->B[SUB]: Передаем получателю сообщение
132
- B[PUB for App1]->App1 Broker[SUB]: Отправляем ответ "Pong!"
133
- App1 Broker[SUB]->App1 Broker[PUB]: Перебрасываем запрос в PUB сокет
134
- App1 Broker[PUB]->A[SUB]: Отправитель получает ответ
135
- </pre></div><script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
34
+ a = A.new
35
+ b = A.new
136
36
 
137
- Балансировка
138
- ============
139
-
140
- Так как это ZMQ, мы можем очень просто изменить тип получения сообщения. Например, добавив балансер. Теперь можно запускать процессы со ссылкой на этот балансер.
141
-
142
- Zactor.start :balancer => "0.0.0.0:4000"
143
-
144
- У нас получится примерно следующая схема:
145
-
146
- ![ZMQ](images/zmq2.png)
147
-
148
- Теперь наш ping можно отправлять в балансер, а отвечать будет один из подключенных воркеров.
149
-
150
- ping Zactor.get_actor("b", :host => "0.0.0.0:4000")
37
+ a.tree.set_parent b
38
+ b.tree.childs # => [a]
39
+ b.tree.map { |o| o } # => [b, a]
151
40
 
41
+ А при использовании методов относящихся к интерфейсу мы явно видим к какому же интерфейсу он относится. Всем профит!
152
42
 
153
- Протокол обмена
154
- ===============
43
+ В интерфейсе доступен метод owner, возвращающий родительский объект. У класса интерфейса есть <tt>interface_base</tt>, возвращающий класс, куда интерфейс был заинклужен.
155
44
 
45
+ Помимо инстанс метода, создается так же класс-метод. В него можно передать блок, который выполнится в скоупе класса интерфейса. Сам метод возвращает класс интерфейса.
156
46
 
157
- Perfomance
158
- ==========
47
+ module StateMachine
48
+ extend RubyInterface
49
+ interface :state_machine do
50
+ def self.state(name)
51
+ puts "New state #{name}"
52
+ end
53
+ end
54
+ end
159
55
 
160
- А хрен его знает, толком не мерялось ничего :)
56
+ class A
57
+ include StateMachine
58
+
59
+ state_machine do
60
+ state(:parked) # => New state parked
61
+ state(:idling) # => New state idling
62
+ end
63
+ end
64
+
65
+ При наследовании класса с интерфейсом, создается новый класс интерфейса и наследуется от предыдущего, т.е. повторяет иерархию класса, в который он включен.
161
66
 
162
- TODO
163
- ====
67
+ Если в блоке <tt>interface</tt> вызывается метод <tt>interfaced</tt>, то исполнение блока, передаваемого <tt>interfaced</tt>
68
+ происходит после добавления интерфейса в класс, в контексте этого класса.
164
69
 
165
- * Сделать событие отваливания объектов. Наверное, что-то вроде простого аналога link в эрланге.
166
- * Добавить таймауты для запросов с коллбэками. Сейчас они будут висеть бесконечно и засрут память.
167
- * Доступ к отправителю в колллбэке запроса. В случае с балансировкой он будет не тем же, кому мы посылали сообщение
70
+ Пример:
71
+
72
+ module A
73
+ extend RubyInterface
74
+ interface :int do
75
+ interfaced do
76
+ def baz
77
+ self.class.int_interface.foo
78
+ end
79
+ end
80
+
81
+ def self.foo
82
+ "bar"
83
+ end
84
+ end
85
+ end
86
+
87
+ class B
88
+ include A
89
+ end
90
+
91
+ B.new.baz # => "bar"
92
+
93
+ В каждом модуле может быть определено произвольное количество интерфейсов
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+
1
3
  require 'rubygems'
2
4
  require 'bundler'
3
5
  begin
@@ -0,0 +1,3 @@
1
+ module RubyInterface
2
+ VERSION = "0.0.5"
3
+ end
@@ -0,0 +1,8 @@
1
+ class RubyInterfaceHandler < YARD::Handlers::Ruby::Base
2
+ handles method_call(:interface)
3
+
4
+ def process
5
+ parse_block(statement.last.last)
6
+ rescue YARD::Handlers::NamespaceMissingError
7
+ end
8
+ end
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'active_support/core_ext/class/attribute'
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ module RubyInterface
6
+ def interface(method_name, &interface_body)
7
+ mod_inst = self.const_set("#{method_name.to_s.camelize}InstanceMethods", Module.new)
8
+ mod_inst.module_eval <<-EOT, __FILE__, __LINE__ + 1
9
+ def #{method_name}
10
+ @#{method_name}_interface ||= self.class.#{method_name}_interface.new(self)
11
+ end
12
+ EOT
13
+
14
+
15
+ mod_class = self.const_set("#{method_name.to_s.camelize}ClassMethods", Module.new)
16
+ mod_class.module_eval <<-EOT, __FILE__, __LINE__ + 1
17
+ def #{method_name}(&blk)
18
+ self.#{method_name}_interface.class_eval(&blk) if blk
19
+ self.#{method_name}_interface
20
+ end
21
+
22
+ def inherited(subclass)
23
+ new_class = subclass.const_set("#{method_name.to_s.camelize}InterfaceClass", Class.new(self.#{method_name}_interface))
24
+ new_class.interface_base = subclass
25
+ subclass.#{method_name}_interface = new_class
26
+ super
27
+ end
28
+ EOT
29
+
30
+ interface_module = self
31
+
32
+ add_interface do |base|
33
+ base.send(:class_attribute, "#{method_name}_interface")
34
+ interface_class = base.const_set("#{method_name.to_s.camelize}InterfaceClass", Class.new(RubyInterface::InterfaceClass))
35
+ interface_class.interface_base = base
36
+ interface_class.class_eval(&interface_body) if interface_body
37
+ base.send("#{method_name}_interface=", interface_class)
38
+ base.extend mod_class
39
+ base.send :include, mod_inst
40
+ base.class_eval(&interface_class.interfaced) if interface_class.interfaced
41
+ end
42
+
43
+ interface_module.define_singleton_method(:included) do |base|
44
+ @_deps.each {|d| d.call(base)}
45
+ end
46
+ end
47
+
48
+ private
49
+ def add_interface &block
50
+ @_deps ||= []
51
+ @_deps << block
52
+ end
53
+
54
+ class InterfaceClass
55
+ class_attribute :interface_base
56
+ attr_accessor :owner
57
+ def initialize(owner)
58
+ @owner = owner
59
+ end
60
+
61
+ class << self
62
+ def interfaced(&block)
63
+ if block_given?
64
+ @_interfaced_block = block
65
+ else
66
+ @_interfaced_block
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,15 +1,15 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "zactor/version"
3
+ require "ruby-interface/version"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = %q{zactor}
7
- s.version = Zactor::VERSION
8
- s.summary = "Zactor"
7
+ s.version = RubyInterface::VERSION
8
+ s.summary = "Ruby interface"
9
9
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
- s.authors = ["Andrew Rudenko"]
10
+ s.authors = ["Andrew Rudenko", "Nick Recobra"]
11
11
  s.date = %q{2011-03-24}
12
- s.description = %q{Zactor}
12
+ s.description = %q{Ruby interface}
13
13
  s.email = %q{ceo@prepor.ru}
14
14
 
15
15
  s.files = `git ls-files`.split("\n")
@@ -17,12 +17,6 @@ Gem::Specification.new do |s|
17
17
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
18
  s.require_paths = ["lib"]
19
19
 
20
- s.add_dependency('ffi', ["> 0.1"])
21
- s.add_dependency('ruby-interface', ["> 0"])
22
- s.add_dependency('ffi-rzmq', ["> 0.1"])
23
- s.add_dependency('em-zeromq', ["> 0.1"])
24
- s.add_dependency('bson', ["> 0.1"])
25
- s.add_dependency('bson_ext', ["> 0.1"])
26
20
  s.add_dependency('activesupport', ["> 0.1"])
27
- end
28
-
21
+ s.add_dependency(%q<i18n>, ["> 0.1"])
22
+ end
@@ -0,0 +1,127 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'spec_helper'
3
+
4
+ describe RubyInterface do
5
+ def make_interface
6
+ mod = Module.new
7
+ mod.extend(RubyInterface)
8
+ mod
9
+ end
10
+
11
+ describe "Создание интерфейса" do
12
+ let(:mod) { make_interface.tap { |v| v.interface(:test) } }
13
+
14
+ it "должен вызываться interfaced, если определен" do
15
+ @proc = proc {}
16
+ klass = Class.new
17
+ mod.interface :int do
18
+ interfaced(&@proc)
19
+ end
20
+ mock(klass).class_eval(&@proc)
21
+ mock(klass).class_eval(&@proc)
22
+ klass.send :include, mod
23
+ end
24
+
25
+ describe "Класс с интерфейсом" do
26
+ subject { klass }
27
+ let(:klass) { Class.new.tap { |v| v.send :include, mod } }
28
+ let(:interface_klass) { klass.test_interface }
29
+ its(:test) { should eq(interface_klass) }
30
+
31
+ it "может дополнять класс интерфейса" do
32
+ interface_klass.respond_to?(:test).should be_false
33
+ klass.test do
34
+ mattr_accessor :test
35
+ end
36
+ interface_klass.respond_to?(:test).should be_true
37
+ end
38
+
39
+ it "должен содержать класс интерфейса" do
40
+ klass::TestInterfaceClass.ancestors.should include(RubyInterface::InterfaceClass)
41
+ klass.test_interface.should eq(klass::TestInterfaceClass)
42
+ end
43
+
44
+ describe "Класс интерфейса" do
45
+ subject { interface_klass }
46
+ its(:interface_base) { should eq(klass) }
47
+
48
+ describe "Объект с интерфейсом" do
49
+ subject { obj }
50
+ let(:obj) { klass.new }
51
+ its(:test) { should be_kind_of(interface_klass)}
52
+ describe "Объект интерфейса" do
53
+ subject { interface_obj }
54
+ let(:interface_obj) { obj.test }
55
+ its(:owner) { should eq(obj) }
56
+ let(:mod) do
57
+ make_interface.tap do |v|
58
+ v.interface(:test) do
59
+ def foo
60
+ "bar"
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ it "должен поддерживать методы определенные в интерфейсе" do
67
+ interface_obj.foo.should eq('bar')
68
+ end
69
+ end
70
+ end
71
+
72
+ describe "при наследовании" do
73
+ let(:klass2) { Class.new(klass) }
74
+ it "должен создавать новый интерфейс-класс, наследуя старый" do
75
+ klass2.test.ancestors.should include(interface_klass)
76
+ end
77
+ it "новый интерфейс-класс должен ссылаться на новый класс" do
78
+ klass2.test.interface_base.should eq(klass2)
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ end
85
+ end
86
+
87
+ describe "Модуль с двумя интерфейсами" do
88
+ before(:each) do
89
+ @mod = Module.new
90
+ @mod.send :extend, RubyInterface
91
+ @mod.interface :first do
92
+ interfaced do
93
+ def baz
94
+ first.foo
95
+ end
96
+ end
97
+
98
+ def foo
99
+ "bar"
100
+ end
101
+ end
102
+
103
+ @mod.interface :second do
104
+ interfaced do
105
+ self.second_interface.foo
106
+ end
107
+
108
+ def self.foo
109
+ "baz"
110
+ end
111
+ end
112
+ end
113
+
114
+ it "не должно происходить ошибок при подключении модуля с двумя интерфейсами" do
115
+ b = Class.new
116
+ lambda { b.send :include, @mod }.should_not raise_error
117
+ end
118
+
119
+ it "должны появиться интерфейсы с методами" do
120
+ b = Class.new
121
+ b.send :include, @mod
122
+ bb = b.new
123
+ bb.baz.should eq("bar")
124
+ bb.second_interface.foo.should eq("baz")
125
+ end
126
+ end
127
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
3
  require 'rspec'
4
- require 'zactor'
5
4
  require 'rr'
5
+ require 'ruby_interface'
6
6
 
7
7
  # Requires supporting files with custom matchers and macros, etc,
8
8
  # in ./support/ and its subdirectories.