Skip to content

Commit f499c9a

Browse files
dafyddcrosbyfacebook-github-bot
authored andcommitted
Move executable to bin
Summary: This is to get the cookbook tree in line with the bookworm-specific github repo, while still working when run within the cookbook. Change bookworm.rb into a wrapper script, next diff on the stack will do some fancy LOAD_PATH tricks that makes it clearer why I'm using a new wrapper. Differential Revision: D68651635 fbshipit-source-id: 03a8e31f5aa928e6a8847185f799d54b0f4526ea
1 parent 73ed835 commit f499c9a

File tree

2 files changed

+317
-296
lines changed

2 files changed

+317
-296
lines changed
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
#!/opt/chef-workstation/embedded/bin/ruby
2+
# Copyright (c) 2022-present, Meta Platforms, Inc. and affiliates
3+
# All rights reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
require 'optparse'
18+
module Bookworm
19+
class CLIParser
20+
def initialize
21+
parser = ::OptionParser.new
22+
23+
parser.banner = 'Usage: bookworm.rb [options]'
24+
25+
# TODO(dcrosby) explicitly output to stdout?
26+
# parser.on(
27+
# '--output TYPE',
28+
# '(STUB) Configure output type for report. Options: plain (default), JSON',
29+
# )
30+
31+
parser.on(
32+
'--report CLASS',
33+
"Give the (class) name of the report you'd like",
34+
)
35+
36+
parser.on(
37+
'--list-reports',
38+
'Get the (class) names of available reports',
39+
)
40+
41+
parser.on(
42+
'--list-rules',
43+
'Get the (class) names of available inference rules',
44+
)
45+
46+
parser.separator ''
47+
parser.separator 'Debugging options:'
48+
49+
# TODO(dcrosby) add verbose mode
50+
# parser.on(
51+
# '--verbose',
52+
# 'Enable verbose mode',
53+
# )
54+
55+
parser.on(
56+
'--profiler',
57+
'Enable profiler for performance debugging (requires ruby-prof)',
58+
)
59+
60+
parser.on(
61+
'--irb-config-step',
62+
'Open IRB REPL after loading configuration',
63+
)
64+
65+
parser.on(
66+
'--irb-crawl-step',
67+
'Open IRB REPL after crawler has run',
68+
)
69+
70+
parser.on(
71+
'--irb-infer-step',
72+
'Open IRB REPL after inference has run',
73+
)
74+
75+
parser.on(
76+
'--irb-report-step',
77+
'Open IRB REPL after report is generated',
78+
)
79+
80+
@parser = parser
81+
end
82+
83+
def help
84+
@parser.help
85+
end
86+
87+
def parse
88+
options = {}
89+
@parser.parse(ARGV, :into => options)
90+
options
91+
end
92+
end
93+
end
94+
parser = Bookworm::CLIParser.new
95+
options = parser.parse
96+
97+
if options[:profiler]
98+
require 'ruby-prof'
99+
Bookworm::Profile = RubyProf::Profile.new
100+
Bookworm::Profile.start
101+
end
102+
103+
# We require the libraries *after* the profiler has a chance to start,
104+
# also means faster `bookworm -h` response
105+
require 'set'
106+
require_relative '../exceptions'
107+
require_relative '../keys'
108+
require_relative '../configuration'
109+
require_relative '../crawler'
110+
require_relative '../knowledge_base'
111+
require_relative '../infer_engine'
112+
require_relative '../report_builder'
113+
114+
module Bookworm
115+
class ClassLoadError < RuntimeError; end
116+
117+
# Class to hold state of a Bookworm run
118+
class Run
119+
attr_reader :cli_help_message, :config, :report_src_dirs, :rule_src_dirs, :action, :irb_breakpoints, :report_name
120+
121+
def initialize(cli_options, cli_help_message)
122+
@cli_help_message = cli_help_message
123+
validate_cli_args(cli_options)
124+
set_irb_breakpoints(cli_options)
125+
generate_config
126+
validate_config_file
127+
load_src_dirs
128+
determine_action(cli_options)
129+
binding.irb if irb_breakpoint?('config') # rubocop:disable Lint/Debugger
130+
end
131+
132+
def set_irb_breakpoints(options)
133+
@irb_breakpoints = []
134+
%w{config crawl infer report}.each do |bp|
135+
@irb_breakpoints << bp if options["irb-#{bp}-step".to_sym]
136+
end
137+
end
138+
139+
def irb_breakpoint?(str)
140+
@irb_breakpoints.include?(str)
141+
end
142+
143+
def do_action
144+
case @action
145+
when :"list-reports"
146+
list_reports
147+
when :"list-rules"
148+
list_rules
149+
when :report
150+
generate_report
151+
end
152+
end
153+
154+
def determine_action(options)
155+
[:"list-reports", :"list-rules", :report].each do |a|
156+
if options[a]
157+
if @action
158+
cli_fail 'Multiple actions specified, check your arguments'
159+
else
160+
@action = a
161+
end
162+
end
163+
end
164+
@report_name = options[:report]
165+
end
166+
167+
def generate_config
168+
# TODO(dcrosby) read CLI for config file path
169+
@config = Bookworm::Configuration.new
170+
end
171+
172+
def cli_fail(msg)
173+
puts "#{msg}\n\n#{@cli_help_message}"
174+
exit(false)
175+
end
176+
177+
def validate_cli_args(options)
178+
unless options[:"list-reports"] || options[:"list-rules"]
179+
unless options[:report]
180+
cli_fail 'No report name given, take a look at bookworm --list-reports'
181+
end
182+
end
183+
end
184+
185+
def validate_config_file
186+
if @config.source_dirs.nil? || @config.source_dirs.empty?
187+
fail 'configuration source_dirs cannot be empty'
188+
end
189+
end
190+
191+
def load_src_dirs
192+
@report_src_dirs = ["#{__dir__}/../reports/"]
193+
if Dir.exist? "#{@config.system_contrib_dir}/reports"
194+
@report_src_dirs.append "#{@config.system_contrib_dir}/reports"
195+
end
196+
@rule_src_dirs = ["#{__dir__}/../rules/"]
197+
if Dir.exist? "#{@config.system_contrib_dir}/rules/"
198+
@rule_src_dirs.append "#{@config.system_contrib_dir}/rules/"
199+
end
200+
end
201+
202+
def list_reports
203+
@report_src_dirs.each do |d|
204+
Bookworm.load_reports_dir d
205+
end
206+
207+
puts Bookworm::Reports.constants.map { |x|
208+
"#{x}\t#{Module.const_get("Bookworm::Reports::#{x}")&.description}"
209+
}.sort.join("\n")
210+
end
211+
212+
def list_rules
213+
@rule_src_dirs.each do |d|
214+
Bookworm.load_rules_dir d
215+
end
216+
puts Bookworm::InferRules.constants.map { |x|
217+
"#{x}\t#{Module.const_get("Bookworm::InferRules::#{x}")&.description}"
218+
}.sort.join("\n")
219+
end
220+
221+
def generate_report
222+
load_classes_for_report
223+
crawl_source
224+
make_inferences
225+
build_report
226+
end
227+
228+
def load_classes_for_report
229+
@report_src_dirs.each do |d|
230+
231+
Bookworm.load_report_class @report_name, :dir => d
232+
break
233+
rescue Bookworm::ClassLoadError
234+
# puts "Unable to load report #{report_name}, take a look at bookworm --list-reports\n\n"
235+
236+
end
237+
unless Bookworm::Reports.const_defined?(@report_name.to_sym)
238+
cli_fail "Unable to load report #{@report_name}, take a look at bookworm --list-reports"
239+
end
240+
241+
# To keep processing to only what is needed, the rules are specified within
242+
# the report. From those rules, we gather the keys that actually need to be
243+
# crawled (instead of crawling everything)
244+
# TODO(dcrosby) recursively check rules for dependency keys
245+
@rules = Bookworm.get_report_rules(@report_name)
246+
@rules.each do |rule|
247+
@rule_src_dirs.each do |d|
248+
249+
Bookworm.load_rule_class rule, :dir => d
250+
break
251+
rescue Bookworm::ClassLoadError
252+
# puts "Unable to load rule #{rule}, take a look at bookworm --list-rules\n\n"
253+
254+
end
255+
unless Bookworm::InferRules.const_defined?(rule.to_sym)
256+
cli_fail "Unable to load rule #{rule}, take a look at bookworm --list-rules"
257+
end
258+
end
259+
end
260+
261+
def crawl_source
262+
# Determine necessary keys to crawl
263+
keys = @rules.map { |r| Module.const_get("Bookworm::InferRules::#{r}")&.keys }.flatten.uniq
264+
265+
# The crawler determines the files that need to be processed
266+
# It currently converts Ruby source files to AST/objects (that may change)
267+
processed_files = Bookworm::Crawler.new(config, :keys => keys).processed_files
268+
269+
# The knowledge base is what we know about the files (AST, paths,
270+
# digested information from inference rules, etc)
271+
@knowledge_base = Bookworm::KnowledgeBase.new(processed_files)
272+
273+
binding.irb if irb_breakpoint?('crawl') # rubocop:disable Lint/Debugger
274+
end
275+
276+
def make_inferences
277+
# InferEngine takes the crawler output in the knowledge base and runs a series
278+
# of Infer rules against the source AST (and more) to build a knowledge base
279+
# around the source
280+
# It runs classes within the Bookworm::InferRules module namespace
281+
engine = Bookworm::InferEngine.new(@knowledge_base, @rules)
282+
@knowledge_base = engine.knowledge_base
283+
284+
binding.irb if irb_breakpoint?('infer') # rubocop:disable Lint/Debugger
285+
end
286+
287+
def build_report
288+
# The ReportBuilder takes a knowledge base and generates a report
289+
# with each class in the Bookworm::Reports module namespace
290+
Bookworm::ReportBuilder.new(@knowledge_base, @report_name)
291+
292+
binding.irb if irb_breakpoint?('report') # rubocop:disable Lint/Debugger
293+
end
294+
end
295+
end
296+
297+
# TODO refactor this file so the below PROGRAM_NAME hack isn't necessary
298+
if __FILE__ == $PROGRAM_NAME || $PROGRAM_NAME == './bin/bookworm' || $PROGRAM_NAME == './bookworm.rb'
299+
run = Bookworm::Run.new(options, parser.help)
300+
run.do_action
301+
end
302+
303+
if options[:profiler]
304+
result = Bookworm::Profile.stop
305+
printer = RubyProf::GraphPrinter.new(result)
306+
path = "#{Dir.tmpdir}/bookworm_profile-#{DateTime.now.iso8601(4)}.out"
307+
printer = ::RubyProf::GraphPrinter.new(result)
308+
File.open(path, 'w+') do |file|
309+
printer.print(file)
310+
end
311+
puts "Wrote profiler output to #{path}"
312+
end

0 commit comments

Comments
 (0)