nanoc-cli 4.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/NEWS.md +3 -0
- data/README.md +3 -0
- data/lib/nanoc-cli.rb +3 -0
- data/lib/nanoc/cli.rb +237 -0
- data/lib/nanoc/cli/ansi_string_colorizer.rb +30 -0
- data/lib/nanoc/cli/cleaning_stream.rb +160 -0
- data/lib/nanoc/cli/command_runner.rb +74 -0
- data/lib/nanoc/cli/commands/compile.rb +57 -0
- data/lib/nanoc/cli/commands/create-site.rb +257 -0
- data/lib/nanoc/cli/commands/nanoc.rb +42 -0
- data/lib/nanoc/cli/commands/prune.rb +49 -0
- data/lib/nanoc/cli/commands/shell.rb +57 -0
- data/lib/nanoc/cli/commands/show-data.rb +185 -0
- data/lib/nanoc/cli/commands/show-plugins.rb +97 -0
- data/lib/nanoc/cli/commands/view.rb +68 -0
- data/lib/nanoc/cli/compile_listeners/abstract.rb +58 -0
- data/lib/nanoc/cli/compile_listeners/aggregate.rb +50 -0
- data/lib/nanoc/cli/compile_listeners/debug_printer.rb +100 -0
- data/lib/nanoc/cli/compile_listeners/diff_generator.rb +101 -0
- data/lib/nanoc/cli/compile_listeners/file_action_printer.rb +80 -0
- data/lib/nanoc/cli/compile_listeners/timing_recorder.rb +170 -0
- data/lib/nanoc/cli/error_handler.rb +365 -0
- data/lib/nanoc/cli/logger.rb +77 -0
- data/lib/nanoc/cli/stack_trace_writer.rb +51 -0
- data/lib/nanoc/cli/stream_cleaners/abstract.rb +23 -0
- data/lib/nanoc/cli/stream_cleaners/ansi_colors.rb +15 -0
- data/lib/nanoc/cli/stream_cleaners/utf8.rb +20 -0
- data/lib/nanoc/cli/transform.rb +18 -0
- data/lib/nanoc/cli/version.rb +7 -0
- metadata +127 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nanoc::CLI::CompileListeners
|
4
|
+
class Aggregate < Abstract
|
5
|
+
def initialize(command_runner:, site:, compiler:)
|
6
|
+
@site = site
|
7
|
+
@compiler = compiler
|
8
|
+
@command_runner = command_runner
|
9
|
+
|
10
|
+
@listener_classes = self.class.default_listener_classes
|
11
|
+
end
|
12
|
+
|
13
|
+
def start
|
14
|
+
setup_listeners
|
15
|
+
end
|
16
|
+
|
17
|
+
def stop
|
18
|
+
teardown_listeners
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.default_listener_classes
|
22
|
+
[
|
23
|
+
Nanoc::CLI::CompileListeners::DiffGenerator,
|
24
|
+
Nanoc::CLI::CompileListeners::DebugPrinter,
|
25
|
+
Nanoc::CLI::CompileListeners::TimingRecorder,
|
26
|
+
Nanoc::CLI::CompileListeners::FileActionPrinter,
|
27
|
+
]
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def setup_listeners
|
33
|
+
res = @compiler.run_until_reps_built
|
34
|
+
reps = res.fetch(:reps)
|
35
|
+
|
36
|
+
@listeners =
|
37
|
+
@listener_classes
|
38
|
+
.select { |klass| klass.enable_for?(@command_runner, @site) }
|
39
|
+
.map { |klass| klass.new(reps: reps) }
|
40
|
+
|
41
|
+
@listeners.each(&:start_safely)
|
42
|
+
end
|
43
|
+
|
44
|
+
def teardown_listeners
|
45
|
+
return unless @listeners
|
46
|
+
|
47
|
+
@listeners.reverse_each(&:stop_safely)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nanoc::CLI::CompileListeners
|
4
|
+
class DebugPrinter < Abstract
|
5
|
+
# @see Listener#enable_for?
|
6
|
+
def self.enable_for?(command_runner, _site)
|
7
|
+
command_runner.debug?
|
8
|
+
end
|
9
|
+
|
10
|
+
COLOR_MAP = {
|
11
|
+
'compilation' => "\e[31m",
|
12
|
+
'content' => "\e[32m",
|
13
|
+
'filtering' => "\e[33m",
|
14
|
+
'dependency_tracking' => "\e[34m",
|
15
|
+
'phase' => "\e[35m",
|
16
|
+
'stage' => "\e[36m",
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
# @see Listener#start
|
20
|
+
def start
|
21
|
+
on(:compilation_started) do |rep|
|
22
|
+
log('compilation', "Started compilation of #{rep}")
|
23
|
+
end
|
24
|
+
|
25
|
+
on(:compilation_ended) do |rep|
|
26
|
+
log('compilation', "Ended compilation of #{rep}")
|
27
|
+
log('compilation', '')
|
28
|
+
end
|
29
|
+
|
30
|
+
on(:compilation_suspended) do |rep, target_rep, snapshot_name|
|
31
|
+
log('compilation', "Suspended compilation of #{rep}: depends on #{target_rep}, snapshot #{snapshot_name}")
|
32
|
+
end
|
33
|
+
|
34
|
+
on(:cached_content_used) do |rep|
|
35
|
+
log('content', "Used cached compiled content for #{rep} instead of recompiling")
|
36
|
+
end
|
37
|
+
|
38
|
+
on(:snapshot_created) do |rep, snapshot_name|
|
39
|
+
log('content', "Snapshot #{snapshot_name} created for #{rep}")
|
40
|
+
end
|
41
|
+
|
42
|
+
on(:filtering_started) do |rep, filter_name|
|
43
|
+
log('filtering', "Started filtering #{rep} with #{filter_name}")
|
44
|
+
end
|
45
|
+
|
46
|
+
on(:filtering_ended) do |rep, filter_name|
|
47
|
+
log('filtering', "Ended filtering #{rep} with #{filter_name}")
|
48
|
+
end
|
49
|
+
|
50
|
+
on(:dependency_created) do |src, dst|
|
51
|
+
log('dependency_tracking', "Dependency created from #{src.inspect} onto #{dst.inspect}")
|
52
|
+
end
|
53
|
+
|
54
|
+
on(:phase_started) do |phase_name, rep|
|
55
|
+
log('phase', "Phase started: #{phase_name} (rep: #{rep})")
|
56
|
+
end
|
57
|
+
|
58
|
+
on(:phase_yielded) do |phase_name, rep|
|
59
|
+
log('phase', "Phase yielded: #{phase_name} (rep: #{rep})")
|
60
|
+
end
|
61
|
+
|
62
|
+
on(:phase_resumed) do |phase_name, rep|
|
63
|
+
log('phase', "Phase resumed: #{phase_name} (rep: #{rep})")
|
64
|
+
end
|
65
|
+
|
66
|
+
on(:phase_ended) do |phase_name, rep|
|
67
|
+
log('phase', "Phase ended: #{phase_name} (rep: #{rep})")
|
68
|
+
end
|
69
|
+
|
70
|
+
on(:phase_aborted) do |phase_name, rep|
|
71
|
+
log('phase', "Phase aborted: #{phase_name} (rep: #{rep})")
|
72
|
+
end
|
73
|
+
|
74
|
+
on(:stage_started) do |stage_name|
|
75
|
+
log('stage', "Stage started: #{stage_name}")
|
76
|
+
end
|
77
|
+
|
78
|
+
on(:stage_ended) do |stage_name|
|
79
|
+
log('stage', "Stage ended: #{stage_name}")
|
80
|
+
end
|
81
|
+
|
82
|
+
on(:stage_aborted) do |stage_name|
|
83
|
+
log('stage', "Stage aborted: #{stage_name}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def log(progname, msg)
|
88
|
+
logger.info(progname) { msg }
|
89
|
+
end
|
90
|
+
|
91
|
+
def logger
|
92
|
+
@_logger ||=
|
93
|
+
Logger.new($stdout).tap do |l|
|
94
|
+
l.formatter = proc do |_severity, datetime, progname, msg|
|
95
|
+
"*** #{datetime.strftime('%H:%M:%S.%L')} #{COLOR_MAP[progname]}#{msg}\e[0m\n"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nanoc::CLI::CompileListeners
|
4
|
+
class DiffGenerator < Abstract
|
5
|
+
class Differ
|
6
|
+
def initialize(path, str_a, str_b)
|
7
|
+
@path = path
|
8
|
+
@str_a = str_a
|
9
|
+
@str_b = str_b
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
run
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def run
|
19
|
+
lines_a = @str_a.lines.map(&:chomp)
|
20
|
+
lines_b = @str_b.lines.map(&:chomp)
|
21
|
+
|
22
|
+
diffs = Diff::LCS.diff(lines_a, lines_b)
|
23
|
+
|
24
|
+
output = +''
|
25
|
+
output << "--- #{@path}\n"
|
26
|
+
output << "+++ #{@path}\n"
|
27
|
+
|
28
|
+
prev_hunk = hunk = nil
|
29
|
+
file_length_difference = 0
|
30
|
+
diffs.each do |piece|
|
31
|
+
begin
|
32
|
+
hunk = Diff::LCS::Hunk.new(lines_a, lines_b, piece, 3, file_length_difference)
|
33
|
+
file_length_difference = hunk.file_length_difference
|
34
|
+
|
35
|
+
next unless prev_hunk
|
36
|
+
next if hunk.merge(prev_hunk)
|
37
|
+
|
38
|
+
output << prev_hunk.diff(:unified) << "\n"
|
39
|
+
ensure
|
40
|
+
prev_hunk = hunk
|
41
|
+
end
|
42
|
+
end
|
43
|
+
last = prev_hunk.diff(:unified)
|
44
|
+
output << last << "\n"
|
45
|
+
|
46
|
+
output
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# @see Listener#enable_for?
|
51
|
+
def self.enable_for?(command_runner, site)
|
52
|
+
site.config[:enable_output_diff] || command_runner.options[:diff]
|
53
|
+
end
|
54
|
+
|
55
|
+
# @see Listener#start
|
56
|
+
def start
|
57
|
+
setup_diffs
|
58
|
+
|
59
|
+
on(:rep_ready_for_diff) do |raw_path, old_content, new_content|
|
60
|
+
generate_diff_for(raw_path, old_content, new_content)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @see Listener#stop
|
65
|
+
def stop
|
66
|
+
teardown_diffs
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
def setup_diffs
|
72
|
+
@diff_lock = Mutex.new
|
73
|
+
@diff_threads = []
|
74
|
+
FileUtils.rm('output.diff') if File.file?('output.diff')
|
75
|
+
end
|
76
|
+
|
77
|
+
def teardown_diffs
|
78
|
+
@diff_threads.each(&:join)
|
79
|
+
end
|
80
|
+
|
81
|
+
def generate_diff_for(path, old_content, new_content)
|
82
|
+
return if old_content == new_content
|
83
|
+
|
84
|
+
@diff_threads << Thread.new do
|
85
|
+
# Simplify path
|
86
|
+
# FIXME: do not depend on working directory
|
87
|
+
if path.start_with?(Dir.getwd)
|
88
|
+
path = path[(Dir.getwd.size + 1)..path.size]
|
89
|
+
end
|
90
|
+
|
91
|
+
# Generate diff
|
92
|
+
diff = Differ.new(path, old_content, new_content).call
|
93
|
+
|
94
|
+
# Write diff
|
95
|
+
@diff_lock.synchronize do
|
96
|
+
File.open('output.diff', 'a') { |io| io.write(diff) }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nanoc::CLI::CompileListeners
|
4
|
+
class FileActionPrinter < Abstract
|
5
|
+
def initialize(reps:)
|
6
|
+
@reps = reps
|
7
|
+
|
8
|
+
@stopwatches = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# @see Listener#start
|
12
|
+
def start
|
13
|
+
on(:compilation_started) do |rep|
|
14
|
+
@stopwatches[rep] ||= DDMetrics::Stopwatch.new
|
15
|
+
@stopwatches[rep].start
|
16
|
+
end
|
17
|
+
|
18
|
+
on(:compilation_suspended) do |rep|
|
19
|
+
@stopwatches[rep].stop
|
20
|
+
end
|
21
|
+
|
22
|
+
cached_reps = Set.new
|
23
|
+
on(:cached_content_used) do |rep|
|
24
|
+
cached_reps << rep
|
25
|
+
end
|
26
|
+
|
27
|
+
on(:rep_write_enqueued) do |rep|
|
28
|
+
@stopwatches[rep].stop
|
29
|
+
end
|
30
|
+
|
31
|
+
on(:rep_write_started) do |rep, _raw_path|
|
32
|
+
@stopwatches[rep].start
|
33
|
+
end
|
34
|
+
|
35
|
+
on(:rep_write_ended) do |rep, _binary, path, is_created, is_modified|
|
36
|
+
@stopwatches[rep].stop
|
37
|
+
duration = @stopwatches[rep].duration
|
38
|
+
|
39
|
+
action =
|
40
|
+
if is_created then :create
|
41
|
+
elsif is_modified then :update
|
42
|
+
elsif cached_reps.include?(rep) then :cached
|
43
|
+
else :identical
|
44
|
+
end
|
45
|
+
level =
|
46
|
+
if is_created then :high
|
47
|
+
elsif is_modified then :high
|
48
|
+
else :low
|
49
|
+
end
|
50
|
+
|
51
|
+
# FIXME: do not depend on working directory
|
52
|
+
if path.start_with?(Dir.getwd)
|
53
|
+
path = path[(Dir.getwd.size + 1)..path.size]
|
54
|
+
end
|
55
|
+
|
56
|
+
log(level, action, path, duration)
|
57
|
+
end
|
58
|
+
|
59
|
+
on(:file_pruned) do |path|
|
60
|
+
Nanoc::CLI::Logger.instance.file(:high, :delete, path)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# @see Listener#stop
|
65
|
+
def stop
|
66
|
+
@reps.reject(&:compiled?).each do |rep|
|
67
|
+
raw_paths = rep.raw_paths.values.flatten.uniq
|
68
|
+
raw_paths.each do |raw_path|
|
69
|
+
log(:low, :skip, raw_path, nil)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def log(level, action, path, duration)
|
77
|
+
Nanoc::CLI::Logger.instance.file(level, action, path, duration)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Nanoc::CLI::CompileListeners
|
4
|
+
class TimingRecorder < Abstract
|
5
|
+
attr_reader :stages_summary
|
6
|
+
attr_reader :phases_summary
|
7
|
+
attr_reader :outdatedness_rules_summary
|
8
|
+
attr_reader :filters_summary
|
9
|
+
|
10
|
+
# @see Listener#enable_for?
|
11
|
+
def self.enable_for?(_command_runner, _site)
|
12
|
+
Nanoc::CLI.verbosity >= 1
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Enumerable<Nanoc::Core::ItemRep>] reps
|
16
|
+
def initialize(reps:)
|
17
|
+
@reps = reps
|
18
|
+
|
19
|
+
@stages_summary = DDMetrics::Summary.new
|
20
|
+
@phases_summary = DDMetrics::Summary.new
|
21
|
+
@outdatedness_rules_summary = DDMetrics::Summary.new
|
22
|
+
@filters_summary = DDMetrics::Summary.new
|
23
|
+
@load_stores_summary = DDMetrics::Summary.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# @see Listener#start
|
27
|
+
def start
|
28
|
+
on(:stage_ran) do |duration, klass|
|
29
|
+
@stages_summary.observe(duration, name: klass.to_s.sub(/.*::/, ''))
|
30
|
+
end
|
31
|
+
|
32
|
+
on(:outdatedness_rule_ran) do |duration, klass|
|
33
|
+
@outdatedness_rules_summary.observe(duration, name: klass.to_s.sub(/.*::/, ''))
|
34
|
+
end
|
35
|
+
|
36
|
+
filter_stopwatches = {}
|
37
|
+
|
38
|
+
on(:filtering_started) do |rep, _filter_name|
|
39
|
+
stopwatch_stack = filter_stopwatches.fetch(rep) { filter_stopwatches[rep] = [] }
|
40
|
+
stopwatch_stack << DDMetrics::Stopwatch.new
|
41
|
+
stopwatch_stack.last.start
|
42
|
+
end
|
43
|
+
|
44
|
+
on(:filtering_ended) do |rep, filter_name|
|
45
|
+
stopwatch = filter_stopwatches.fetch(rep).pop
|
46
|
+
stopwatch.stop
|
47
|
+
|
48
|
+
@filters_summary.observe(stopwatch.duration, name: filter_name.to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
on(:store_loaded) do |duration, klass|
|
52
|
+
@load_stores_summary.observe(duration, name: klass.to_s)
|
53
|
+
end
|
54
|
+
|
55
|
+
on(:compilation_suspended) do |rep, _target_rep, _snapshot_name|
|
56
|
+
filter_stopwatches.fetch(rep).each(&:stop)
|
57
|
+
end
|
58
|
+
|
59
|
+
on(:compilation_started) do |rep|
|
60
|
+
filter_stopwatches.fetch(rep, []).each(&:start)
|
61
|
+
end
|
62
|
+
|
63
|
+
setup_phase_notifications
|
64
|
+
end
|
65
|
+
|
66
|
+
# @see Listener#stop
|
67
|
+
def stop
|
68
|
+
print_profiling_feedback
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
def setup_phase_notifications
|
74
|
+
stopwatches = {}
|
75
|
+
|
76
|
+
on(:phase_started) do |phase_name, rep|
|
77
|
+
stopwatch = stopwatches[[phase_name, rep]] = DDMetrics::Stopwatch.new
|
78
|
+
stopwatch.start
|
79
|
+
end
|
80
|
+
|
81
|
+
on(:phase_ended) do |phase_name, rep|
|
82
|
+
stopwatch = stopwatches[[phase_name, rep]]
|
83
|
+
stopwatch.stop
|
84
|
+
|
85
|
+
@phases_summary.observe(stopwatch.duration, name: phase_name)
|
86
|
+
end
|
87
|
+
|
88
|
+
on(:phase_yielded) do |phase_name, rep|
|
89
|
+
stopwatch = stopwatches[[phase_name, rep]]
|
90
|
+
stopwatch.stop
|
91
|
+
end
|
92
|
+
|
93
|
+
on(:phase_resumed) do |phase_name, rep|
|
94
|
+
# It probably looks weird that a phase can be resumed even though it was not suspended earlier. This can happen when compilation is suspended, where you’d get the sequence started -> suspended -> started -> resumed.
|
95
|
+
stopwatch = stopwatches[[phase_name, rep]]
|
96
|
+
stopwatch.start unless stopwatch.running?
|
97
|
+
end
|
98
|
+
|
99
|
+
on(:phase_aborted) do |phase_name, rep|
|
100
|
+
stopwatch = stopwatches[[phase_name, rep]]
|
101
|
+
stopwatch.stop if stopwatch.running?
|
102
|
+
|
103
|
+
@phases_summary.observe(stopwatch.duration, name: phase_name)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def table_for_summary(name, summary)
|
108
|
+
headers = [name.to_s, 'count', 'min', '.50', '.90', '.95', 'max', 'tot']
|
109
|
+
|
110
|
+
rows = summary.map do |label, stats|
|
111
|
+
name = label.fetch(:name)
|
112
|
+
|
113
|
+
count = stats.count
|
114
|
+
min = stats.min
|
115
|
+
p50 = stats.quantile(0.50)
|
116
|
+
p90 = stats.quantile(0.90)
|
117
|
+
p95 = stats.quantile(0.95)
|
118
|
+
tot = stats.sum
|
119
|
+
max = stats.max
|
120
|
+
|
121
|
+
[name, count.to_s] + [min, p50, p90, p95, max, tot].map { |r| "#{format('%4.2f', r)}s" }
|
122
|
+
end
|
123
|
+
|
124
|
+
[headers] + rows
|
125
|
+
end
|
126
|
+
|
127
|
+
def table_for_summary_durations(name, summary)
|
128
|
+
headers = [name.to_s, 'tot']
|
129
|
+
|
130
|
+
rows = summary.map do |label, stats|
|
131
|
+
name = label.fetch(:name)
|
132
|
+
[name, "#{format('%4.2f', stats.sum)}s"]
|
133
|
+
end
|
134
|
+
|
135
|
+
[headers] + rows
|
136
|
+
end
|
137
|
+
|
138
|
+
def print_profiling_feedback
|
139
|
+
print_table_for_summary(:filters, @filters_summary)
|
140
|
+
print_table_for_summary(:phases, @phases_summary) if Nanoc::CLI.verbosity >= 2
|
141
|
+
print_table_for_summary_duration(:stages, @stages_summary) if Nanoc::CLI.verbosity >= 2
|
142
|
+
print_table_for_summary(:outdatedness_rules, @outdatedness_rules_summary) if Nanoc::CLI.verbosity >= 2
|
143
|
+
print_table_for_summary_duration(:load_stores, @load_stores_summary) if Nanoc::CLI.verbosity >= 2
|
144
|
+
print_ddmemoize_metrics if Nanoc::CLI.verbosity >= 2
|
145
|
+
end
|
146
|
+
|
147
|
+
def print_table_for_summary(name, summary)
|
148
|
+
return unless summary.any?
|
149
|
+
|
150
|
+
puts
|
151
|
+
print_table(table_for_summary(name, summary))
|
152
|
+
end
|
153
|
+
|
154
|
+
def print_table_for_summary_duration(name, summary)
|
155
|
+
return unless summary.any?
|
156
|
+
|
157
|
+
puts
|
158
|
+
print_table(table_for_summary_durations(name, summary))
|
159
|
+
end
|
160
|
+
|
161
|
+
def print_ddmemoize_metrics
|
162
|
+
puts
|
163
|
+
DDMemoize.print_metrics
|
164
|
+
end
|
165
|
+
|
166
|
+
def print_table(rows)
|
167
|
+
puts DDMetrics::Table.new(rows).to_s
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|