nanoc 4.7.0 → 4.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -4
  3. data/NEWS.md +12 -1
  4. data/README.md +1 -1
  5. data/lib/nanoc/base/entities/directed_graph.rb +16 -0
  6. data/lib/nanoc/base/entities/identifiable_collection.rb +24 -8
  7. data/lib/nanoc/base/entities/outdatedness_reasons.rb +5 -0
  8. data/lib/nanoc/base/errors.rb +11 -6
  9. data/lib/nanoc/base/memoization.rb +6 -23
  10. data/lib/nanoc/base/services/compiler/phases/abstract.rb +3 -3
  11. data/lib/nanoc/base/services/compiler/phases/cache.rb +1 -3
  12. data/lib/nanoc/base/services/compiler/phases/mark_done.rb +1 -3
  13. data/lib/nanoc/base/services/compiler/phases/recalculate.rb +1 -3
  14. data/lib/nanoc/base/services/compiler/phases/resume.rb +1 -3
  15. data/lib/nanoc/base/services/compiler/phases/write.rb +1 -3
  16. data/lib/nanoc/base/services/filter.rb +15 -0
  17. data/lib/nanoc/base/services/item_rep_selector.rb +1 -1
  18. data/lib/nanoc/base/services/outdatedness_checker.rb +3 -1
  19. data/lib/nanoc/base/services/outdatedness_rule.rb +7 -0
  20. data/lib/nanoc/base/services/outdatedness_rules.rb +16 -0
  21. data/lib/nanoc/cli/commands/compile.rb +12 -411
  22. data/lib/nanoc/cli/commands/compile_listeners/abstract.rb +30 -0
  23. data/lib/nanoc/cli/commands/compile_listeners/debug_printer.rb +34 -0
  24. data/lib/nanoc/cli/commands/compile_listeners/diff_generator.rb +91 -0
  25. data/lib/nanoc/cli/commands/compile_listeners/file_action_printer.rb +61 -0
  26. data/lib/nanoc/cli/commands/compile_listeners/stack_prof_profiler.rb +22 -0
  27. data/lib/nanoc/cli/commands/compile_listeners/timing_recorder.rb +174 -0
  28. data/lib/nanoc/filters/xsl.rb +2 -0
  29. data/lib/nanoc/version.rb +1 -1
  30. data/spec/nanoc/base/directed_graph_spec.rb +54 -0
  31. data/spec/nanoc/base/errors/dependency_cycle_spec.rb +32 -0
  32. data/spec/nanoc/base/filter_spec.rb +36 -0
  33. data/spec/nanoc/base/services/compiler/phases/abstract_spec.rb +23 -11
  34. data/spec/nanoc/base/services/outdatedness_rules_spec.rb +41 -0
  35. data/spec/nanoc/cli/commands/compile/file_action_printer_spec.rb +1 -1
  36. data/spec/nanoc/cli/commands/compile/timing_recorder_spec.rb +124 -54
  37. data/spec/nanoc/cli/commands/compile_spec.rb +1 -1
  38. data/spec/nanoc/regressions/gh_1040_spec.rb +1 -1
  39. data/spec/nanoc/regressions/gh_924_spec.rb +89 -0
  40. data/test/base/test_compiler.rb +1 -1
  41. data/test/base/test_memoization.rb +6 -0
  42. data/test/cli/commands/test_compile.rb +2 -2
  43. metadata +12 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a139ed28840cdd9e8d815959a6ce67a2a3d64629
4
- data.tar.gz: 443fad3be7c31740a6b0378e37b358355f4cf451
3
+ metadata.gz: 3697f07907752b5c0010bbf3fb01e701d6128bba
4
+ data.tar.gz: 70dfb18ac4973838113380a201ddde4468f2b13b
5
5
  SHA512:
6
- metadata.gz: 54e42408687a77e4775218127f65e92780702969c3f18e8d4dbd723b7428d6f0a8eec603545ab092f69de699af67af0861ae690b142b5e9314ac4c893ef8830b
7
- data.tar.gz: 66dc4eb54985f21f9a6614b366fbcd4b0dc4b8e3dc3cd4fe0e7b6f5c3497432f91e0221e3868cf8c2a2bb60df04dd18d8a0909508e3ef6aad2a15e45ef2cf8f2
6
+ metadata.gz: e8810bd89fcd0e3c65f647ec3b99a76454dc2fa35169b5b4e66317f178b260e2e8229686342aee8c83929e7de04b323c98137c8c8b690c7b1259fabed99e2147
7
+ data.tar.gz: 144aacfa4f5981109e514c8c33cde796598e86ec27c2b6d115ff2c4b497dc25b5d7a5950a071ad4e23dce5c68063b699b3fb42258f6b0f7c31ded9a81c311177
data/Gemfile.lock CHANGED
@@ -1,6 +1,6 @@
1
1
  GIT
2
2
  remote: https://github.com/bbatsov/rubocop.git
3
- revision: 7623afc89d97b6149067428e1cb8d69a1786d0b3
3
+ revision: a047a11633e104d5ee9ca4d2957e4b1cf571f30b
4
4
  specs:
5
5
  rubocop (0.47.1)
6
6
  parser (>= 2.3.3.1, < 3.0)
@@ -27,7 +27,7 @@ GIT
27
27
  PATH
28
28
  remote: .
29
29
  specs:
30
- nanoc (4.7.0)
30
+ nanoc (4.7.1)
31
31
  cri (~> 2.3)
32
32
  ddplugin (~> 1.0)
33
33
  hamster (~> 3.0)
@@ -211,7 +211,7 @@ GEM
211
211
  fog-voxel (0.1.0)
212
212
  fog-core
213
213
  fog-xml
214
- fog-vsphere (1.7.1)
214
+ fog-vsphere (1.8.0)
215
215
  fog-core
216
216
  rbvmomi (~> 1.9)
217
217
  fog-xenserver (0.3.0)
@@ -347,7 +347,7 @@ GEM
347
347
  trollop (2.1.2)
348
348
  typogruby (1.0.18)
349
349
  rubypants
350
- uglifier (3.1.7)
350
+ uglifier (3.1.8)
351
351
  execjs (>= 0.3.0, < 3)
352
352
  unicode-display_width (1.1.3)
353
353
  url (0.3.2)
data/NEWS.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # Nanoc news
2
2
 
3
+ ## 4.7.1 (2017-03-19)
4
+
5
+ Fixes:
6
+
7
+ * Fixed issue with `:xsl` filter not recompiling when it should (#924, #1127)
8
+
9
+ Enhancements:
10
+
11
+ * Made `compile --verbose` print percentiles rather than averages (#1122)
12
+ * Improved dependency cycle error messages (#1123)
13
+
3
14
  ## 4.7 (2017-03-15)
4
15
 
5
16
  Features:
6
17
 
7
- * Added `:erubi` filter (#1103)
18
+ * Added `:erubi` filter (#1103) [Jan M. Faber]
8
19
  * Added `write ext: 'something'` shortcut (#1079)
9
20
 
10
21
  ## 4.6.4 (2017-03-10)
data/README.md CHANGED
@@ -19,4 +19,4 @@ Contributions are greatly appreciated! Consult the [Development guidelines](http
19
19
 
20
20
  Many thanks to everyone who has contributed to Nanoc in one way or another:
21
21
 
22
- Ale Muñoz, Alexander Mankuta, Andy Drop, Arnau Siches, Ben Armston, Bil Bas, Brian Candler, Bruno Dufour, Cédric Boutillier, Chris Chapman, Chris Eppstein, Christian Plessl, Colin Barrett, Colin Seymour, Croath Liu, Damien Pollet, Dan Callahan, Daniel Hofstetter, Daniel Mendler, Daniel Wollschlaeger, David Alexander, David Everitt, Denis Defreyne, Dennis Sutch, Devon Luke Buchanan, Dmitry Bilunov, Eric Sunshine, Erik Hollensbe, Fabian Buch, Felix Hanley, Garen Torikian, Go Maeda, Grégory Karékinian, Gregory Pakosz, Guilherme Garnier, Hugo Peixoto, Jack Chu, Jake Benilov, Jasper Van der Jeugt, Jeff Forcier, Jim Mendenhall, John Nishinaga, Justin Clift, Justin Hileman, Kevin Lynagh, Lorin Werthen, Louis T., Lucas Vuotto, Mathias Bynens, Matt Keveney, Matthew Frazier, Matthias Beyer, Matthias Reitinger, Matthias Vallentin, Micha Rosenbaum, Michal Cichra, Michal Papis, Mike Pennisi, Nelson Chen, Nicky Peeters, Nikhil Marathe, Oliver Byford, Paul Boone, Peter Aronoff, Raphael von der Grün, Rémi Barraquand, Remko Tronçon, Riley Goodside, Ruben Verborgh, Scott Vokes, Šime Ramov, Simon South, Spencer Whitt, Stanley Rost, Starr Horne, Stefan Bühler, Stuart Montgomery, Takashi Uchibe, Toon Willems, Tuomas Kareinen, Ursula Kallio, Vincent Driessen, Vlatko Kosturjak, whitequark, Xavier Shay, Yannick Ihmels, Zaiste de Grengolada
22
+ Ale Muñoz, Alexander Mankuta, Andy Drop, Arnau Siches, Ben Armston, Bil Bas, Brian Candler, Bruno Dufour, Cédric Boutillier, Chris Chapman, Chris Eppstein, Christian Plessl, Colin Barrett, Colin Seymour, Croath Liu, Damien Pollet, Dan Callahan, Daniel Hofstetter, Daniel Mendler, Daniel Wollschlaeger, David Alexander, David Everitt, Denis Defreyne, Dennis Sutch, Devon Luke Buchanan, Dmitry Bilunov, Eric Sunshine, Erik Hollensbe, Fabian Buch, Felix Hanley, Garen Torikian, Go Maeda, Grégory Karékinian, Gregory Pakosz, Guilherme Garnier, Hugo Peixoto, Jack Chu, Jake Benilov, Jan M. Faber, Jasper Van der Jeugt, Jeff Forcier, Jim Mendenhall, John Nishinaga, Justin Clift, Justin Hileman, Kevin Lynagh, Lorin Werthen, Louis T., Lucas Vuotto, Mathias Bynens, Matt Keveney, Matthew Frazier, Matthias Beyer, Matthias Reitinger, Matthias Vallentin, Micha Rosenbaum, Michal Cichra, Michal Papis, Mike Pennisi, Nelson Chen, Nicky Peeters, Nikhil Marathe, Oliver Byford, Paul Boone, Peter Aronoff, Raphael von der Grün, Rémi Barraquand, Remko Tronçon, Riley Goodside, Ruben Verborgh, Scott Vokes, Šime Ramov, Simon South, Spencer Whitt, Stanley Rost, Starr Horne, Stefan Bühler, Stuart Montgomery, Takashi Uchibe, Toon Willems, Tuomas Kareinen, Ursula Kallio, Vincent Driessen, Vlatko Kosturjak, whitequark, Xavier Shay, Yannick Ihmels, Zaiste de Grengolada
@@ -158,6 +158,22 @@ module Nanoc::Int
158
158
 
159
159
  # @group Querying the graph
160
160
 
161
+ def any_cycle
162
+ path = [@vertices.keys.first]
163
+
164
+ loop do
165
+ nexts = direct_successors_of(path.last)
166
+ cycle_start_index = path.find_index { |node| nexts.include?(node) }
167
+ if cycle_start_index
168
+ break path[cycle_start_index..-1]
169
+ elsif nexts.empty?
170
+ break nil
171
+ else
172
+ path << nexts.sample
173
+ end
174
+ end
175
+ end
176
+
161
177
  # Returns the direct predecessors of the given vertex, i.e. the vertices
162
178
  # x where there is an edge from x to the given vertex y.
163
179
  #
@@ -4,6 +4,7 @@ module Nanoc::Int
4
4
  include Nanoc::Int::ContractsSupport
5
5
  include Enumerable
6
6
 
7
+ extend Nanoc::Int::Memoization
7
8
  extend Forwardable
8
9
 
9
10
  def_delegator :@objects, :each
@@ -29,15 +30,10 @@ module Nanoc::Int
29
30
 
30
31
  contract C::Any => C::Maybe[C::RespondTo[:identifier]]
31
32
  def [](arg)
32
- case arg
33
- when Nanoc::Identifier
34
- object_with_identifier(arg)
35
- when String
36
- object_with_identifier(arg) || object_matching_glob(arg)
37
- when Regexp
38
- @objects.find { |i| i.identifier.to_s =~ arg }
33
+ if frozen?
34
+ get_memoized(arg)
39
35
  else
40
- raise ArgumentError, "don’t know how to fetch objects by #{arg.inspect}"
36
+ get_unmemoized(arg)
41
37
  end
42
38
  end
43
39
 
@@ -61,6 +57,26 @@ module Nanoc::Int
61
57
 
62
58
  protected
63
59
 
60
+ contract C::Any => C::Maybe[C::RespondTo[:identifier]]
61
+ def get_unmemoized(arg)
62
+ case arg
63
+ when Nanoc::Identifier
64
+ object_with_identifier(arg)
65
+ when String
66
+ object_with_identifier(arg) || object_matching_glob(arg)
67
+ when Regexp
68
+ @objects.find { |i| i.identifier.to_s =~ arg }
69
+ else
70
+ raise ArgumentError, "don’t know how to fetch objects by #{arg.inspect}"
71
+ end
72
+ end
73
+
74
+ contract C::Any => C::Maybe[C::RespondTo[:identifier]]
75
+ def get_memoized(arg)
76
+ get_unmemoized(arg)
77
+ end
78
+ memoize :get_memoized
79
+
64
80
  def object_with_identifier(identifier)
65
81
  if frozen?
66
82
  @mapping[identifier.to_s]
@@ -58,5 +58,10 @@ module Nanoc::Int
58
58
  'One or more output paths of this item have been modified since the last time the site was compiled.',
59
59
  Props.new(path: true),
60
60
  )
61
+
62
+ UsesAlwaysOutdatedFilter = Generic.new(
63
+ 'This item rep uses one or more filters that cannot track dependencies, and will thus always be considered as outdated.',
64
+ Props.new(raw_content: true, attributes: true, compiled_content: true),
65
+ )
61
66
  end
62
67
  end
@@ -68,12 +68,17 @@ module Nanoc::Int
68
68
 
69
69
  # Error that is raised during site compilation when an item (directly or
70
70
  # indirectly) includes its own item content, leading to endless recursion.
71
- class RecursiveCompilation < Generic
72
- # @param [Array<Nanoc::Int::ItemRep>] reps A list of item representations
73
- # that mutually depend on each other
74
- def initialize(reps)
75
- list = reps.map(&:inspect).join("\n")
76
- super("The site cannot be compiled because the following items mutually depend on each other:\n#{list}.")
71
+ class DependencyCycle < Generic
72
+ def initialize(graph)
73
+ cycle = graph.any_cycle
74
+
75
+ msg_bits = []
76
+ msg_bits << 'The site cannot be compiled because there is a dependency cycle:'
77
+ msg_bits << ''
78
+ cycle.each.with_index { |r, i| msg_bits << " (#{i + 1}) item #{r.item.identifier}, rep #{r.name.inspect}, depends on" }
79
+ msg_bits.last << ' (1)'
80
+
81
+ super(msg_bits.map { |x| x + "\n" }.join(''))
77
82
  end
78
83
  end
79
84
 
@@ -5,23 +5,6 @@ module Nanoc::Int
5
5
  #
6
6
  # @api private
7
7
  module Memoization
8
- class Wrapper
9
- attr_reader :ref
10
-
11
- def initialize(ref)
12
- @ref = ref
13
- end
14
-
15
- def inspect
16
- obj = @ref.object
17
- if obj
18
- obj.inspect
19
- else
20
- '<garbage collected>'
21
- end
22
- end
23
- end
24
-
25
8
  class Value
26
9
  attr_reader :value
27
10
 
@@ -69,20 +52,20 @@ module Nanoc::Int
69
52
  original_method_name = '__nonmemoized_' + method_name.to_s
70
53
  alias_method original_method_name, method_name
71
54
 
55
+ instance_cache = Hash.new { |hash, key| hash[key] = {} }
56
+
72
57
  define_method(method_name) do |*args|
73
- @__memoization_cache ||= {}
74
- @__memoization_cache[method_name] ||= {}
75
- method_cache = @__memoization_cache[method_name]
58
+ instance_method_cache = instance_cache[self]
76
59
 
77
60
  value = NONE
78
- if method_cache.key?(args)
79
- object = method_cache[args].ref.object
61
+ if instance_method_cache.key?(args)
62
+ object = instance_method_cache[args].object
80
63
  value = object ? object.value : NONE
81
64
  end
82
65
 
83
66
  if value.equal?(NONE)
84
67
  send(original_method_name, *args).tap do |r|
85
- method_cache[args] = Wrapper.new(Ref::SoftReference.new(Value.new(r)))
68
+ instance_method_cache[args] = Ref::SoftReference.new(Value.new(r))
86
69
  end
87
70
  else
88
71
  value
@@ -2,8 +2,7 @@ module Nanoc::Int::Compiler::Phases
2
2
  class Abstract
3
3
  include Nanoc::Int::ContractsSupport
4
4
 
5
- def initialize(wrapped:, name:)
6
- @name = name
5
+ def initialize(wrapped:)
7
6
  @wrapped = wrapped
8
7
  end
9
8
 
@@ -28,7 +27,8 @@ module Nanoc::Int::Compiler::Phases
28
27
  private
29
28
 
30
29
  def notify(sym, rep)
31
- Nanoc::Int::NotificationCenter.post(sym, @name, rep)
30
+ name = self.class.to_s.sub(/^.*::/, '')
31
+ Nanoc::Int::NotificationCenter.post(sym, name, rep)
32
32
  end
33
33
  end
34
34
  end
@@ -4,10 +4,8 @@ module Nanoc::Int::Compiler::Phases
4
4
  class Cache < Abstract
5
5
  include Nanoc::Int::ContractsSupport
6
6
 
7
- NAME = 'cache'.freeze
8
-
9
7
  def initialize(wrapped:, compiled_content_cache:, snapshot_repo:)
10
- super(wrapped: wrapped, name: NAME)
8
+ super(wrapped: wrapped)
11
9
 
12
10
  @compiled_content_cache = compiled_content_cache
13
11
  @snapshot_repo = snapshot_repo
@@ -2,10 +2,8 @@ module Nanoc::Int::Compiler::Phases
2
2
  class MarkDone < Abstract
3
3
  include Nanoc::Int::ContractsSupport
4
4
 
5
- NAME = 'mark_done'.freeze
6
-
7
5
  def initialize(wrapped:, outdatedness_store:)
8
- super(wrapped: wrapped, name: NAME)
6
+ super(wrapped: wrapped)
9
7
 
10
8
  @outdatedness_store = outdatedness_store
11
9
  end
@@ -4,10 +4,8 @@ module Nanoc::Int::Compiler::Phases
4
4
  class Recalculate < Abstract
5
5
  include Nanoc::Int::ContractsSupport
6
6
 
7
- NAME = 'recalculate'.freeze
8
-
9
7
  def initialize(action_provider:, dependency_store:, compilation_context:)
10
- super(wrapped: nil, name: NAME)
8
+ super(wrapped: nil)
11
9
 
12
10
  @action_provider = action_provider
13
11
  @dependency_store = dependency_store
@@ -3,10 +3,8 @@ module Nanoc::Int::Compiler::Phases
3
3
  class Resume < Abstract
4
4
  include Nanoc::Int::ContractsSupport
5
5
 
6
- NAME = 'resume'.freeze
7
-
8
6
  def initialize(wrapped:)
9
- super(wrapped: wrapped, name: NAME)
7
+ super(wrapped: wrapped)
10
8
  end
11
9
 
12
10
  contract Nanoc::Int::ItemRep, C::KeywordArgs[is_outdated: C::Bool], C::Func[C::None => C::Any] => C::Any
@@ -2,10 +2,8 @@ module Nanoc::Int::Compiler::Phases
2
2
  class Write < Abstract
3
3
  include Nanoc::Int::ContractsSupport
4
4
 
5
- NAME = 'write'.freeze
6
-
7
5
  def initialize(snapshot_repo:, wrapped:)
8
- super(wrapped: wrapped, name: NAME)
6
+ super(wrapped: wrapped)
9
7
 
10
8
  @snapshot_repo = snapshot_repo
11
9
  end
@@ -93,6 +93,21 @@ module Nanoc
93
93
  (@to || :text) == :binary
94
94
  end
95
95
 
96
+ # @return [Boolean]
97
+ #
98
+ # @api private
99
+ def always_outdated?
100
+ @always_outdated || false
101
+ end
102
+
103
+ # Marks this filter as always causing the item rep to be outdated. This is useful for filters
104
+ # that cannot do dependency tracking properly.
105
+ #
106
+ # @return [void]
107
+ def always_outdated
108
+ @always_outdated = true
109
+ end
110
+
96
111
  # @overload requires(*requires)
97
112
  # Sets the required libraries for this filter.
98
113
  # @param [Array<String>] requires A list of library names that are required
@@ -30,7 +30,7 @@ module Nanoc::Int
30
30
 
31
31
  # Check whether everything was compiled
32
32
  unless graph.vertices.empty?
33
- raise Nanoc::Int::Errors::RecursiveCompilation.new(graph.vertices)
33
+ raise Nanoc::Int::Errors::DependencyCycle.new(graph)
34
34
  end
35
35
  end
36
36
 
@@ -19,6 +19,7 @@ module Nanoc::Int
19
19
  Rules::NotWritten,
20
20
  Rules::CodeSnippetsModified,
21
21
  Rules::ConfigurationModified,
22
+ Rules::UsesAlwaysOutdatedFilter,
22
23
  ].freeze
23
24
 
24
25
  RULES_FOR_LAYOUT =
@@ -26,6 +27,7 @@ module Nanoc::Int
26
27
  Rules::RulesModified,
27
28
  Rules::ContentModified,
28
29
  Rules::AttributesModified,
30
+ Rules::UsesAlwaysOutdatedFilter,
29
31
  ].freeze
30
32
 
31
33
  contract C::KeywordArgs[outdatedness_checker: OutdatednessChecker, reps: Nanoc::Int::ItemRepRepo] => C::Any
@@ -56,7 +58,7 @@ module Nanoc::Int
56
58
  rules.inject(status) do |acc, rule|
57
59
  if !acc.useful_to_apply?(rule)
58
60
  acc
59
- elsif rule.instance.apply(obj, @outdatedness_checker)
61
+ elsif rule.instance.call(obj, @outdatedness_checker)
60
62
  acc.update(rule.instance.reason)
61
63
  else
62
64
  acc
@@ -4,6 +4,13 @@ module Nanoc::Int
4
4
  include Nanoc::Int::ContractsSupport
5
5
  include Singleton
6
6
 
7
+ def call(obj, outdatedness_checker)
8
+ Nanoc::Int::NotificationCenter.post(:outdatedness_rule_started, self.class, obj)
9
+ apply(obj, outdatedness_checker)
10
+ ensure
11
+ Nanoc::Int::NotificationCenter.post(:outdatedness_rule_ended, self.class, obj)
12
+ end
13
+
7
14
  def apply(_obj, _outdatedness_checker)
8
15
  raise NotImplementedError.new('Nanoc::Int::OutdatednessRule subclasses must implement ##reason, and #apply')
9
16
  end
@@ -117,5 +117,21 @@ module Nanoc::Int
117
117
  paths_old != paths_new
118
118
  end
119
119
  end
120
+
121
+ class UsesAlwaysOutdatedFilter < OutdatednessRule
122
+ def reason
123
+ Nanoc::Int::OutdatednessReasons::UsesAlwaysOutdatedFilter
124
+ end
125
+
126
+ def apply(obj, outdatedness_checker)
127
+ mem = outdatedness_checker.action_provider.memory_for(obj)
128
+
129
+ mem
130
+ .select { |a| a.is_a?(Nanoc::Int::ProcessingActions::Filter) }
131
+ .map { |a| Nanoc::Filter.named(a.filter_name) }
132
+ .compact
133
+ .any?(&:always_outdated?)
134
+ end
135
+ end
120
136
  end
121
137
  end
@@ -5,414 +5,15 @@ Compile all items of the current site.
5
5
  EOS
6
6
  flag nil, :profile, 'profile compilation' if Nanoc::Feature.enabled?(Nanoc::Feature::PROFILER)
7
7
 
8
+ require_relative 'compile_listeners/abstract'
9
+ require_relative 'compile_listeners/debug_printer'
10
+ require_relative 'compile_listeners/diff_generator'
11
+ require_relative 'compile_listeners/file_action_printer'
12
+ require_relative 'compile_listeners/stack_prof_profiler'
13
+ require_relative 'compile_listeners/timing_recorder'
14
+
8
15
  module Nanoc::CLI::Commands
9
16
  class Compile < ::Nanoc::CLI::CommandRunner
10
- # Listens to compilation events and reacts to them. This abstract class
11
- # does not have a real implementation; subclasses should override {#start}
12
- # and set up notifications to listen to.
13
- #
14
- # @abstract Subclasses must override {#start} and may override {#stop}.
15
- class Listener
16
- def initialize(*); end
17
-
18
- # @param [Nanoc::CLI::CommandRunner] command_runner The command runner for this listener
19
- #
20
- # @return [Boolean] true if this listener should be enabled for the given command runner, false otherwise
21
- #
22
- # @abstract Returns `true` by default, but subclasses may override this.
23
- def self.enable_for?(command_runner) # rubocop:disable Lint/UnusedMethodArgument
24
- true
25
- end
26
-
27
- # Starts the listener. Subclasses should override this method and set up listener notifications.
28
- #
29
- # @return [void]
30
- #
31
- # @abstract
32
- def start
33
- raise NotImplementedError, 'Subclasses of Listener should implement #start'
34
- end
35
-
36
- # Stops the listener. The default implementation removes self from all notification center observers.
37
- #
38
- # @return [void]
39
- def stop; end
40
-
41
- # @api private
42
- def start_safely
43
- start
44
- @_started = true
45
- end
46
-
47
- # @api private
48
- def stop_safely
49
- stop if @_started
50
- @_started = false
51
- end
52
- end
53
-
54
- # Generates diffs for every output file written
55
- class DiffGenerator < Listener
56
- # @see Listener#enable_for?
57
- def self.enable_for?(command_runner)
58
- command_runner.site.config[:enable_output_diff]
59
- end
60
-
61
- # @see Listener#start
62
- def start
63
- require 'tempfile'
64
- setup_diffs
65
- old_contents = {}
66
- Nanoc::Int::NotificationCenter.on(:will_write_rep, self) do |rep, path|
67
- old_contents[rep] = File.file?(path) ? File.read(path) : nil
68
- end
69
- Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, binary, path, _is_created, _is_modified|
70
- unless binary
71
- new_contents = File.file?(path) ? File.read(path) : nil
72
- if old_contents[rep] && new_contents
73
- generate_diff_for(path, old_contents[rep], new_contents)
74
- end
75
- old_contents.delete(rep)
76
- end
77
- end
78
- end
79
-
80
- # @see Listener#stop
81
- def stop
82
- super
83
-
84
- Nanoc::Int::NotificationCenter.remove(:will_write_rep, self)
85
- Nanoc::Int::NotificationCenter.remove(:rep_written, self)
86
-
87
- teardown_diffs
88
- end
89
-
90
- protected
91
-
92
- def setup_diffs
93
- @diff_lock = Mutex.new
94
- @diff_threads = []
95
- FileUtils.rm('output.diff') if File.file?('output.diff')
96
- end
97
-
98
- def teardown_diffs
99
- @diff_threads.each(&:join)
100
- end
101
-
102
- def generate_diff_for(path, old_content, new_content)
103
- return if old_content == new_content
104
-
105
- @diff_threads << Thread.new do
106
- # Generate diff
107
- diff = diff_strings(old_content, new_content)
108
- diff.sub!(/^--- .*/, '--- ' + path)
109
- diff.sub!(/^\+\+\+ .*/, '+++ ' + path)
110
-
111
- # Write diff
112
- @diff_lock.synchronize do
113
- File.open('output.diff', 'a') { |io| io.write(diff) }
114
- end
115
- end
116
- end
117
-
118
- def diff_strings(a, b)
119
- require 'open3'
120
-
121
- # Create files
122
- Tempfile.open('old') do |old_file|
123
- Tempfile.open('new') do |new_file|
124
- # Write files
125
- old_file.write(a)
126
- old_file.flush
127
- new_file.write(b)
128
- new_file.flush
129
-
130
- # Diff
131
- cmd = ['diff', '-u', old_file.path, new_file.path]
132
- Open3.popen3(*cmd) do |_stdin, stdout, _stderr|
133
- result = stdout.read
134
- return (result == '' ? nil : result)
135
- end
136
- end
137
- end
138
- rescue Errno::ENOENT
139
- warn 'Failed to run `diff`, so no diff with the previously compiled ' \
140
- 'content will be available.'
141
- nil
142
- end
143
- end
144
-
145
- # Records the time spent per filter and per item representation
146
- class TimingRecorder < Listener
147
- # @see Listener#enable_for?
148
- def self.enable_for?(command_runner)
149
- command_runner.options.fetch(:verbose, false)
150
- end
151
-
152
- # @param [Enumerable<Nanoc::Int::ItemRep>] reps
153
- def initialize(reps:)
154
- @reps = reps
155
- end
156
-
157
- # @see Listener#start
158
- def start
159
- @telemetry = Nanoc::Telemetry.new
160
-
161
- stage_stopwatch = Nanoc::Telemetry::Stopwatch.new
162
-
163
- Nanoc::Int::NotificationCenter.on(:stage_started) do |_stage_name|
164
- stage_stopwatch.start
165
- end
166
-
167
- Nanoc::Int::NotificationCenter.on(:stage_ended) do |stage_name|
168
- stage_stopwatch.stop
169
- @telemetry.summary(:stages).observe(stage_stopwatch.duration, stage_name.to_s)
170
- stage_stopwatch = Nanoc::Telemetry::Stopwatch.new
171
- end
172
-
173
- filter_stopwatches = {}
174
-
175
- Nanoc::Int::NotificationCenter.on(:filtering_started) do |rep, _filter_name|
176
- stopwatch_stack = filter_stopwatches.fetch(rep) { filter_stopwatches[rep] = [] }
177
- stopwatch_stack << Nanoc::Telemetry::Stopwatch.new
178
- stopwatch_stack.last.start
179
- end
180
-
181
- Nanoc::Int::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
182
- stopwatch = filter_stopwatches.fetch(rep).pop
183
- stopwatch.stop
184
-
185
- @telemetry.summary(:filters).observe(stopwatch.duration, filter_name.to_s)
186
- end
187
-
188
- Nanoc::Int::NotificationCenter.on(:compilation_suspended) do |rep, _exception|
189
- filter_stopwatches.fetch(rep).each(&:stop)
190
- end
191
-
192
- Nanoc::Int::NotificationCenter.on(:compilation_started) do |rep|
193
- filter_stopwatches.fetch(rep, []).each(&:start)
194
- end
195
-
196
- phase_stopwatches = {}
197
-
198
- Nanoc::Int::NotificationCenter.on(:phase_started) do |phase_name, rep|
199
- stopwatches = phase_stopwatches.fetch(rep) { phase_stopwatches[rep] = {} }
200
- stopwatches[phase_name] = Nanoc::Telemetry::Stopwatch.new.tap(&:start)
201
- end
202
-
203
- Nanoc::Int::NotificationCenter.on(:phase_ended) do |phase_name, rep|
204
- stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
205
- stopwatch.stop
206
-
207
- @telemetry.summary(:phases).observe(stopwatch.duration, phase_name)
208
- end
209
-
210
- Nanoc::Int::NotificationCenter.on(:phase_yielded) do |phase_name, rep|
211
- stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
212
- stopwatch.stop
213
- end
214
-
215
- Nanoc::Int::NotificationCenter.on(:phase_resumed) do |phase_name, rep|
216
- stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
217
- stopwatch.start if stopwatch.stopped?
218
- end
219
-
220
- Nanoc::Int::NotificationCenter.on(:phase_aborted) do |phase_name, rep|
221
- stopwatch = phase_stopwatches.fetch(rep).fetch(phase_name)
222
- stopwatch.stop if stopwatch.running?
223
-
224
- @telemetry.summary(:phases).observe(stopwatch.duration, phase_name)
225
- end
226
- end
227
-
228
- # @see Listener#stop
229
- def stop
230
- print_profiling_feedback
231
- super
232
- end
233
-
234
- protected
235
-
236
- def table_for_summary(name)
237
- headers = [name.to_s, 'count', 'min', 'avg', 'max', 'tot']
238
-
239
- rows = @telemetry.summary(name).map do |filter_name, summary|
240
- count = summary.count
241
- min = summary.min
242
- avg = summary.avg
243
- tot = summary.sum
244
- max = summary.max
245
-
246
- [filter_name, count.to_s] + [min, avg, max, tot].map { |r| "#{format('%4.2f', r)}s" }
247
- end
248
-
249
- [headers] + rows
250
- end
251
-
252
- def table_for_summary_durations(name)
253
- headers = [name.to_s, 'tot']
254
-
255
- rows = @telemetry.summary(:stages).map do |stage_name, summary|
256
- [stage_name, "#{format('%4.2f', summary.sum)}s"]
257
- end
258
-
259
- [headers] + rows
260
- end
261
-
262
- def print_profiling_feedback
263
- print_table_for_summary(:filters)
264
- print_table_for_summary(:phases) if Nanoc::CLI.verbosity >= 2
265
- print_table_for_summary_duration(:stages) if Nanoc::CLI.verbosity >= 2
266
- end
267
-
268
- def print_table_for_summary(name)
269
- return if @telemetry.summary(name).empty?
270
-
271
- puts
272
- print_table(table_for_summary(name))
273
- end
274
-
275
- def print_table_for_summary_duration(name)
276
- return if @telemetry.summary(name).empty?
277
-
278
- puts
279
- print_table(table_for_summary_durations(name))
280
- end
281
-
282
- def print_table(table)
283
- lengths = table.transpose.map { |r| r.map(&:size).max }
284
-
285
- print_row(table[0], lengths)
286
-
287
- puts "#{'─' * lengths[0]}─┼─#{lengths[1..-1].map { |length| '─' * length }.join('───')}"
288
-
289
- table[1..-1].each { |row| print_row(row, lengths) }
290
- end
291
-
292
- def print_row(row, lengths)
293
- values = row.zip(lengths).map { |text, length| text.rjust length }
294
-
295
- puts values[0] + ' │ ' + values[1..-1].join(' ')
296
- end
297
- end
298
-
299
- # Prints debug information (compilation started/ended, filtering started/ended, …)
300
- class DebugPrinter < Listener
301
- # @see Listener#enable_for?
302
- def self.enable_for?(command_runner)
303
- command_runner.debug?
304
- end
305
-
306
- # @see Listener#start
307
- def start
308
- Nanoc::Int::NotificationCenter.on(:compilation_started) do |rep|
309
- puts "*** Started compilation of #{rep.inspect}"
310
- end
311
- Nanoc::Int::NotificationCenter.on(:compilation_ended) do |rep|
312
- puts "*** Ended compilation of #{rep.inspect}"
313
- puts
314
- end
315
- Nanoc::Int::NotificationCenter.on(:compilation_suspended) do |rep, e|
316
- puts "*** Suspended compilation of #{rep.inspect}: #{e.message}"
317
- end
318
- Nanoc::Int::NotificationCenter.on(:cached_content_used) do |rep|
319
- puts "*** Used cached compiled content for #{rep.inspect} instead of recompiling"
320
- end
321
- Nanoc::Int::NotificationCenter.on(:filtering_started) do |rep, filter_name|
322
- puts "*** Started filtering #{rep.inspect} with #{filter_name}"
323
- end
324
- Nanoc::Int::NotificationCenter.on(:filtering_ended) do |rep, filter_name|
325
- puts "*** Ended filtering #{rep.inspect} with #{filter_name}"
326
- end
327
- Nanoc::Int::NotificationCenter.on(:dependency_created) do |src, dst|
328
- puts "*** Dependency created from #{src.inspect} onto #{dst.inspect}"
329
- end
330
- end
331
- end
332
-
333
- # Prints file actions (created, updated, deleted, identical, skipped)
334
- class FileActionPrinter < Listener
335
- def initialize(reps:)
336
- @start_times = {}
337
- @acc_durations = {}
338
-
339
- @reps = reps
340
- end
341
-
342
- # @see Listener#start
343
- def start
344
- Nanoc::Int::NotificationCenter.on(:compilation_started, self) do |rep|
345
- @start_times[rep] = Time.now
346
- @acc_durations[rep] ||= 0.0
347
- end
348
-
349
- Nanoc::Int::NotificationCenter.on(:compilation_suspended, self) do |rep|
350
- @acc_durations[rep] += Time.now - @start_times[rep]
351
- end
352
-
353
- Nanoc::Int::NotificationCenter.on(:rep_written, self) do |rep, _binary, path, is_created, is_modified|
354
- @acc_durations[rep] += Time.now - @start_times[rep]
355
- duration = @acc_durations[rep]
356
-
357
- action =
358
- if is_created then :create
359
- elsif is_modified then :update
360
- else :identical
361
- end
362
- level =
363
- if is_created then :high
364
- elsif is_modified then :high
365
- else :low
366
- end
367
- log(level, action, path, duration)
368
- end
369
- end
370
-
371
- # @see Listener#stop
372
- def stop
373
- super
374
-
375
- Nanoc::Int::NotificationCenter.remove(:compilation_started, self)
376
- Nanoc::Int::NotificationCenter.remove(:compilation_suspended, self)
377
- Nanoc::Int::NotificationCenter.remove(:rep_written, self)
378
-
379
- @reps.reject(&:compiled?).each do |rep|
380
- raw_paths = rep.raw_paths.values.flatten.uniq
381
- raw_paths.each do |raw_path|
382
- log(:low, :skip, raw_path, nil)
383
- end
384
- end
385
- end
386
-
387
- private
388
-
389
- def log(level, action, path, duration)
390
- Nanoc::CLI::Logger.instance.file(level, action, path, duration)
391
- end
392
- end
393
-
394
- # Records a profile using StackProf
395
- class StackProfProfiler < Listener
396
- PROFILE_FILE = 'tmp/stackprof_profile'.freeze
397
-
398
- # @see Listener#enable_for?
399
- def self.enable_for?(command_runner)
400
- command_runner.options.fetch(:profile, false)
401
- end
402
-
403
- # @see Listener#start
404
- def start
405
- require 'stackprof'
406
- StackProf.start(mode: :cpu)
407
- end
408
-
409
- # @see Listener#stop
410
- def stop
411
- StackProf.stop
412
- StackProf.results(PROFILE_FILE)
413
- end
414
- end
415
-
416
17
  attr_accessor :listener_classes
417
18
 
418
19
  def initialize(options, arguments, command)
@@ -439,11 +40,11 @@ module Nanoc::CLI::Commands
439
40
 
440
41
  def default_listener_classes
441
42
  [
442
- Nanoc::CLI::Commands::Compile::StackProfProfiler,
443
- Nanoc::CLI::Commands::Compile::DiffGenerator,
444
- Nanoc::CLI::Commands::Compile::DebugPrinter,
445
- Nanoc::CLI::Commands::Compile::TimingRecorder,
446
- Nanoc::CLI::Commands::Compile::FileActionPrinter,
43
+ Nanoc::CLI::Commands::CompileListeners::StackProfProfiler,
44
+ Nanoc::CLI::Commands::CompileListeners::DiffGenerator,
45
+ Nanoc::CLI::Commands::CompileListeners::DebugPrinter,
46
+ Nanoc::CLI::Commands::CompileListeners::TimingRecorder,
47
+ Nanoc::CLI::Commands::CompileListeners::FileActionPrinter,
447
48
  ]
448
49
  end
449
50