cocoapods-binary-cache 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/lib/cocoapods-binary-cache.rb +4 -0
  3. data/lib/cocoapods-binary-cache/cache/all.rb +9 -0
  4. data/lib/cocoapods-binary-cache/cache/validation_result.rb +69 -0
  5. data/lib/cocoapods-binary-cache/cache/validator.rb +21 -0
  6. data/lib/cocoapods-binary-cache/cache/validator_accumulated.rb +4 -0
  7. data/lib/cocoapods-binary-cache/cache/validator_base.rb +92 -0
  8. data/lib/cocoapods-binary-cache/cache/validator_dependencies_graph.rb +20 -0
  9. data/lib/cocoapods-binary-cache/cache/validator_dev_pods.rb +22 -0
  10. data/lib/cocoapods-binary-cache/cache/validator_exclusion.rb +14 -0
  11. data/lib/cocoapods-binary-cache/cache/validator_non_dev_pods.rb +13 -0
  12. data/lib/cocoapods-binary-cache/cache/validator_with_podfile.rb +9 -0
  13. data/lib/cocoapods-binary-cache/dependencies_graph/dependencies_graph.rb +95 -0
  14. data/lib/cocoapods-binary-cache/dependencies_graph/graph_visualizer.rb +74 -0
  15. data/lib/cocoapods-binary-cache/gem_version.rb +6 -0
  16. data/lib/cocoapods-binary-cache/helper/benchmark_show.rb +11 -0
  17. data/lib/cocoapods-binary-cache/helper/checksum.rb +12 -0
  18. data/lib/cocoapods-binary-cache/helper/json.rb +37 -0
  19. data/lib/cocoapods-binary-cache/helper/lockfile.rb +67 -0
  20. data/lib/cocoapods-binary-cache/helper/path_utils.rb +8 -0
  21. data/lib/cocoapods-binary-cache/helper/podspec.rb +17 -0
  22. data/lib/cocoapods-binary-cache/hooks/post_install.rb +16 -0
  23. data/lib/cocoapods-binary-cache/hooks/pre_install.rb +141 -0
  24. data/lib/cocoapods-binary-cache/main.rb +21 -0
  25. data/lib/cocoapods-binary-cache/pod-binary/LICENSE.txt +22 -0
  26. data/lib/cocoapods-binary-cache/pod-binary/helper/detected_prebuilt_pods/installer.rb +25 -0
  27. data/lib/cocoapods-binary-cache/pod-binary/helper/detected_prebuilt_pods/target_definition.rb +36 -0
  28. data/lib/cocoapods-binary-cache/pod-binary/helper/feature_switches.rb +90 -0
  29. data/lib/cocoapods-binary-cache/pod-binary/helper/names.rb +36 -0
  30. data/lib/cocoapods-binary-cache/pod-binary/helper/passer.rb +25 -0
  31. data/lib/cocoapods-binary-cache/pod-binary/helper/podfile_options.rb +2 -0
  32. data/lib/cocoapods-binary-cache/pod-binary/helper/prebuild_sandbox.rb +71 -0
  33. data/lib/cocoapods-binary-cache/pod-binary/helper/target_checker.rb +45 -0
  34. data/lib/cocoapods-binary-cache/pod-binary/integration.rb +12 -0
  35. data/lib/cocoapods-binary-cache/pod-binary/integration/alter_specs.rb +93 -0
  36. data/lib/cocoapods-binary-cache/pod-binary/integration/patch/embed_framework_script.rb +36 -0
  37. data/lib/cocoapods-binary-cache/pod-binary/integration/patch/resolve_dependencies.rb +23 -0
  38. data/lib/cocoapods-binary-cache/pod-binary/integration/patch/source_installation.rb +28 -0
  39. data/lib/cocoapods-binary-cache/pod-binary/integration/remove_target_files.rb +29 -0
  40. data/lib/cocoapods-binary-cache/pod-binary/integration/source_installer.rb +111 -0
  41. data/lib/cocoapods-binary-cache/pod-binary/integration/validation.rb +20 -0
  42. data/lib/cocoapods-binary-cache/pod-binary/prebuild.rb +224 -0
  43. data/lib/cocoapods-binary-cache/pod-binary/prebuild_dsl.rb +69 -0
  44. data/lib/cocoapods-binary-cache/pod-binary/prebuild_hook.rb +11 -0
  45. data/lib/cocoapods-binary-cache/pod-binary/tool/tool.rb +12 -0
  46. data/lib/cocoapods-binary-cache/pod-rome/LICENSE.txt +22 -0
  47. data/lib/cocoapods-binary-cache/pod-rome/build_framework.rb +247 -0
  48. data/lib/cocoapods-binary-cache/prebuild_cache.rb +49 -0
  49. data/lib/cocoapods-binary-cache/prebuild_output/metadata.rb +47 -0
  50. data/lib/cocoapods-binary-cache/prebuild_output/output.rb +71 -0
  51. data/lib/cocoapods-binary-cache/scheme_editor.rb +35 -0
  52. data/lib/cocoapods-binary-cache/state_store.rb +11 -0
  53. data/lib/cocoapods-binary-cache/ui.rb +9 -0
  54. data/lib/cocoapods_plugin.rb +5 -0
  55. data/lib/command/binary.rb +18 -0
  56. data/lib/command/config.rb +31 -0
  57. data/lib/command/executor/base.rb +20 -0
  58. data/lib/command/executor/fetcher.rb +44 -0
  59. data/lib/command/executor/prebuilder.rb +58 -0
  60. data/lib/command/executor/pusher.rb +19 -0
  61. data/lib/command/executor/visualizer.rb +20 -0
  62. data/lib/command/fetch.rb +23 -0
  63. data/lib/command/helper/zip.rb +20 -0
  64. data/lib/command/prebuild.rb +29 -0
  65. data/lib/command/visualize.rb +30 -0
  66. metadata +193 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 284d9870e8f4938c92a779543fffa2810804096cb6cf8504a68cc20c0307acfe
4
+ data.tar.gz: 7f0ef9d6f2a0afe33cd6fe1c05c77c0ec51d1598f746fbe090faea61ac6cc513
5
+ SHA512:
6
+ metadata.gz: c3287b32dda1506122f0b1d89be9cf413970c8f3389bc538680db8addfe83ca273df223ded9b31dd3c9ee59c5791331ebee87c93f6f85c3437920d29500952ec
7
+ data.tar.gz: 7ff8a381f96027f7f45321ef2979f43e5383b8f7dfb787adafd2c7afa0348454632a7ab73b43cb2ff05a89aa15c1aac6ff54f245192d86549d63521b612f56af
@@ -0,0 +1,4 @@
1
+ # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
2
+ # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
3
+
4
+ require "cocoapods-binary-cache/gem_version"
@@ -0,0 +1,9 @@
1
+ require_relative "validation_result"
2
+ require_relative "validator_base"
3
+ require_relative "validator_accumulated"
4
+ require_relative "validator_with_podfile"
5
+ require_relative "validator_non_dev_pods"
6
+ require_relative "validator_dev_pods"
7
+ require_relative "validator_dependencies_graph"
8
+ require_relative "validator_exclusion"
9
+ require_relative "validator"
@@ -0,0 +1,69 @@
1
+ module PodPrebuild
2
+ class CacheValidationResult
3
+ attr_reader :hit, :missed_with_reasons
4
+
5
+ def initialize(missed_with_reasons = {}, hit = Set.new)
6
+ @missed_with_reasons = missed_with_reasons
7
+ @hit = hit.to_set - missed_with_reasons.keys
8
+ end
9
+
10
+ def missed
11
+ @missed_with_reasons.keys.to_set
12
+ end
13
+
14
+ def missed?(name)
15
+ @missed_with_reasons.key?(name)
16
+ end
17
+
18
+ def hit?(name)
19
+ @hit.include?(name)
20
+ end
21
+
22
+ def include?(name)
23
+ missed?(name) || hit?(name)
24
+ end
25
+
26
+ def merge(other)
27
+ PodPrebuild::CacheValidationResult.new(
28
+ @missed_with_reasons.merge(other.missed_with_reasons),
29
+ @hit + other.hit
30
+ )
31
+ end
32
+
33
+ def update_to(path)
34
+ FileUtils.mkdir_p(File.dirname(path))
35
+ json_file = PodPrebuild::JSONFile.new(path)
36
+ json_file["cache_missed"] = missed.to_a
37
+ json_file["cache_hit"] = hit.to_a
38
+ json_file.save!
39
+ end
40
+
41
+ def keep(names)
42
+ base_names = names.map { |name| name.split("/")[0] }.to_set
43
+ select { |name| base_names.include?(name.split("/")[0]) }
44
+ end
45
+
46
+ def discard(names)
47
+ base_names = names.map { |name| name.split("/")[0] }.to_set
48
+ reject { |name| base_names.include?(name.split("/")[0]) }
49
+ end
50
+
51
+ def select(&predicate)
52
+ PodPrebuild::CacheValidationResult.new(
53
+ @missed_with_reasons.select { |name, _| predicate.call(name) },
54
+ @hit.select(&predicate)
55
+ )
56
+ end
57
+
58
+ def reject(&predicate)
59
+ select { |name| !predicate.call(name) }
60
+ end
61
+
62
+ def print_summary
63
+ Pod::UI.puts "Cache validation: hit (#{@hit.count}) #{@hit.to_a}"
64
+ @missed_with_reasons.each do |name, reason|
65
+ Pod::UI.puts "Cache validation: missed #{name}. Reason: #{reason}".yellow
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,21 @@
1
+ module PodPrebuild
2
+ class CacheValidator < BaseCacheValidator
3
+ def initialize(options)
4
+ super(options)
5
+ @validators = [
6
+ PodPrebuild::PodfileChangesCacheValidator.new(options),
7
+ PodPrebuild::NonDevPodsCacheValidator.new(options)
8
+ ]
9
+ @validators << PodPrebuild::DevPodsCacheValidator.new(options) if Pod::Podfile::DSL.dev_pods_enabled
10
+ @validators << PodPrebuild::DependenciesGraphCacheValidator.new(options)
11
+ @validators << PodPrebuild::ExclusionCacheValidator.new(options)
12
+ end
13
+
14
+ def validate(*)
15
+ @validators.reduce(PodPrebuild::CacheValidationResult.new) do |acc, validator|
16
+ validation = validator.validate(acc)
17
+ validator.is_a?(AccumulatedCacheValidator) ? validation : acc.merge(validation)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ module PodPrebuild
2
+ class AccumulatedCacheValidator < BaseCacheValidator
3
+ end
4
+ end
@@ -0,0 +1,92 @@
1
+ module PodPrebuild
2
+ class BaseCacheValidator
3
+ attr_reader :podfile, :pod_lockfile, :prebuilt_lockfile
4
+ attr_reader :validate_prebuilt_settings, :generated_framework_path
5
+
6
+ def initialize(options)
7
+ @podfile = options[:podfile]
8
+ @pod_lockfile = options[:pod_lockfile] && PodPrebuild::Lockfile.new(options[:pod_lockfile])
9
+ @prebuilt_lockfile = options[:prebuilt_lockfile] && PodPrebuild::Lockfile.new(options[:prebuilt_lockfile])
10
+ @validate_prebuilt_settings = options[:validate_prebuilt_settings]
11
+ @generated_framework_path = options[:generated_framework_path]
12
+ end
13
+
14
+ def validate(*)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def changes_of_prebuilt_lockfile_vs_podfile
19
+ @changes_of_prebuilt_lockfile_vs_podfile ||= Pod::Installer::Analyzer::SpecsState.new(
20
+ @prebuilt_lockfile.lockfile.detect_changes_with_podfile(@podfile)
21
+ )
22
+ end
23
+
24
+ def validate_with_podfile
25
+ changes = changes_of_prebuilt_lockfile_vs_podfile
26
+ missed = changes.added.map { |pod| [pod, "Added from Podfile"] }.to_h
27
+ missed.merge!(changes.changed.map { |pod| [pod, "Updated from Podfile"] }.to_h)
28
+ PodPrebuild::CacheValidationResult.new(missed, changes.unchanged)
29
+ end
30
+
31
+ def validate_pods(options)
32
+ pods = options[:pods]
33
+ subspec_pods = options[:subspec_pods]
34
+ prebuilt_pods = options[:prebuilt_pods]
35
+
36
+ missed = {}
37
+ hit = Set.new
38
+
39
+ check_pod = lambda do |name|
40
+ version = pods[name]
41
+ prebuilt_version = prebuilt_pods[name]
42
+ result = false
43
+ if prebuilt_version.nil?
44
+ missed[name] = "Not available (#{version})"
45
+ elsif prebuilt_version != version
46
+ missed[name] = "Outdated: (prebuilt: #{prebuilt_version}) vs (#{version})"
47
+ else
48
+ settings_diff = incompatible_build_settings(name)
49
+ if settings_diff.empty?
50
+ hit << name
51
+ result = true
52
+ else
53
+ missed[name] = "Incompatible build settings: #{settings_diff}"
54
+ end
55
+ end
56
+ result
57
+ end
58
+
59
+ subspec_pods.each do |parent, children|
60
+ missed_children = children.reject { |child| check_pod.call(child) }
61
+ if missed_children.empty?
62
+ hit << parent
63
+ else
64
+ missed[parent] = "Subspec pods were missed: #{missed_children}"
65
+ end
66
+ end
67
+
68
+ non_subspec_pods = pods.reject { |pod| subspec_pods.include?(pod) }
69
+ non_subspec_pods.each { |pod, _| check_pod.call(pod) }
70
+ PodPrebuild::CacheValidationResult.new(missed, hit)
71
+ end
72
+
73
+ def read_prebuilt_build_settings(name)
74
+ return {} if generated_framework_path.nil?
75
+
76
+ metadata = PodPrebuild::Metadata.in_dir(generated_framework_path + name)
77
+ metadata.build_settings
78
+ end
79
+
80
+ def incompatible_build_settings(name)
81
+ settings_diff = {}
82
+ prebuilt_build_settings = read_prebuilt_build_settings(name)
83
+ validate_prebuilt_settings&.(name)&.each do |key, value|
84
+ prebuilt_value = prebuilt_build_settings[key]
85
+ unless prebuilt_value.nil? || value == prebuilt_value
86
+ settings_diff[key] = { :current => value, :prebuilt => prebuilt_value }
87
+ end
88
+ end
89
+ settings_diff
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,20 @@
1
+ module PodPrebuild
2
+ class DependenciesGraphCacheValidator < AccumulatedCacheValidator
3
+ def validate(accumulated)
4
+ return accumulated if library_evolution_supported? || @pod_lockfile.nil?
5
+
6
+ dependencies_graph = DependenciesGraph.new(@pod_lockfile.lockfile)
7
+ clients = dependencies_graph.get_clients(accumulated.missed.to_a)
8
+ unless Pod::Podfile::DSL.dev_pods_enabled
9
+ clients = clients.reject { |client| @pod_lockfile.dev_pods.keys.include?(client) }
10
+ end
11
+
12
+ missed = clients.map { |client| [client, "Dependencies were missed"] }.to_h
13
+ accumulated.merge(PodPrebuild::CacheValidationResult.new(missed, Set.new))
14
+ end
15
+
16
+ def library_evolution_supported?
17
+ false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ module PodPrebuild
2
+ class DevPodsCacheValidator < BaseCacheValidator
3
+ def initialize(options)
4
+ super(options)
5
+ @sandbox_root = options[:sandbox_root]
6
+ end
7
+
8
+ def validate(*)
9
+ return PodPrebuild::CacheValidationResult.new if @pod_lockfile.nil?
10
+
11
+ # TODO (thuyen): Logic needs to be revised
12
+ # TODO (thuyen): Migrate the code PodCacheValidator.verify_devpod_checksum to this place
13
+ missed_with_checksum, hit_with_checksum = PodCacheValidator.verify_devpod_checksum(
14
+ @sandbox_root,
15
+ @generated_framework_path,
16
+ @pod_lockfile.lockfile
17
+ )
18
+ missed = missed_with_checksum.transform_values { |checksum| "Checksum changed: #{checksum}" }
19
+ PodPrebuild::CacheValidationResult.new(missed, hit_with_checksum.keys.to_set)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module PodPrebuild
2
+ class ExclusionCacheValidator < AccumulatedCacheValidator
3
+ def initialize(options)
4
+ super(options)
5
+ @ignored_pods = options[:ignored_pods] || Set.new
6
+ @prebuilt_pod_names = options[:prebuilt_pod_names]
7
+ end
8
+
9
+ def validate(accumulated)
10
+ validation = @prebuilt_pod_names.nil? ? accumulated : accumulated.keep(@prebuilt_pod_names)
11
+ validation.discard(@ignored_pods)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module PodPrebuild
2
+ class NonDevPodsCacheValidator < BaseCacheValidator
3
+ def validate(*)
4
+ return PodPrebuild::CacheValidationResult.new if @pod_lockfile.nil?
5
+
6
+ validate_pods(
7
+ pods: @pod_lockfile.non_dev_pods,
8
+ subspec_pods: @pod_lockfile.subspec_pods,
9
+ prebuilt_pods: @prebuilt_lockfile.nil? ? {} : @prebuilt_lockfile.non_dev_pods
10
+ )
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module PodPrebuild
2
+ class PodfileChangesCacheValidator < BaseCacheValidator
3
+ def validate(*)
4
+ return PodPrebuild::CacheValidationResult.new if @prebuilt_lockfile.nil? || @podfile.nil?
5
+
6
+ validate_with_podfile
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,95 @@
1
+ # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
2
+ # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
3
+
4
+ require 'rgl/adjacency'
5
+ require 'rgl/dot'
6
+ require_relative 'graph_visualizer'
7
+
8
+ # Using RGL graph because GraphViz doesn't store adjacent of a node/vertex but we need to traverse a substree from any node
9
+ # https://github.com/monora/rgl/blob/master/lib/rgl/adjacency.rb
10
+
11
+ class DependenciesGraph
12
+ def initialize(lockfile)
13
+ @lockfile = lockfile
14
+ @invert_edge = true # A normal edge is an edge (one direction) from library A to library B which is a dependency of A.
15
+ end
16
+
17
+ # Input : a list of library names.
18
+ # Output: a set of library names which are clients (directly and indirectly) of those input libraries.
19
+ def get_clients(libnames)
20
+ result = Set.new()
21
+ libnames.each do |lib|
22
+ if graph.has_vertex?(lib)
23
+ result.merge(traverse_sub_tree(graph, lib))
24
+ else
25
+ puts "Warning: cannot find lib: #{lib}"
26
+ end
27
+ end
28
+ result
29
+ end
30
+
31
+ def write_graphic_file(output_graphic_fmt, filename='graph', highlight_nodes=Set[])
32
+ if !output_graphic_fmt
33
+ puts 'Error: Need graphic format.'
34
+ return
35
+ end
36
+ graph.write_to_graphic_file(output_graphic_fmt, dotfile=filename, options={}, highlight_nodes)
37
+ end
38
+
39
+ private
40
+
41
+ def dependencies
42
+ @dependencies ||= begin
43
+ if @lockfile
44
+ @lockfile.to_hash['PODS']
45
+ else
46
+ nil
47
+ end
48
+ end
49
+ end
50
+
51
+ # Convert array of dictionaries -> a dictionary with format {A: [A's dependencies]}
52
+ def pod_to_dependencies
53
+ dependencies.map { |d| d.is_a?(Hash) ? d : { d => [] } }.reduce({}) { |combined, individual| combined.merge!(individual) }
54
+ end
55
+
56
+ def add_vertex(graph, pod)
57
+ node_name = sanitized_pod_name(pod)
58
+ graph.add_vertex(node_name)
59
+ node_name
60
+ end
61
+
62
+ def sanitized_pod_name(name)
63
+ Pod::Dependency.from_string(name).name
64
+ end
65
+
66
+ def graph
67
+ @graph ||= begin
68
+ graph = RGL::DirectedAdjacencyGraph.new()
69
+
70
+ pod_to_dependencies.each do |pod, dependencies|
71
+ pod_node = add_vertex(graph, pod)
72
+ next if pod_node.nil?
73
+ dependencies.each do |dependency|
74
+ dep_node = add_vertex(graph, dependency)
75
+ next if dep_node.nil?
76
+ if @invert_edge
77
+ graph.add_edge(dep_node, pod_node)
78
+ else
79
+ graph.add_edge(pod_node, dep_node)
80
+ end
81
+ end
82
+ end
83
+ graph
84
+ end
85
+ end
86
+
87
+ def traverse_sub_tree(graph, vertex)
88
+ visited_nodes = Set.new()
89
+ graph.each_adjacent(vertex) do |v|
90
+ visited_nodes.add(v)
91
+ visited_nodes.merge(traverse_sub_tree(graph, v))
92
+ end
93
+ visited_nodes
94
+ end
95
+ end
@@ -0,0 +1,74 @@
1
+ # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved.
2
+ # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file
3
+ # https://github.com/monora/rgl/blob/0b526e16f9fb344abf387f4c5523d7917ce8f4b1/lib/rgl/dot.rb
4
+
5
+ require 'rgl/rdot'
6
+
7
+ module RGL
8
+ module Graph
9
+ def to_dot_graph(params={}, highlight_nodes)
10
+ params['name'] ||= self.class.name.gsub(/:/, '_')
11
+ fontsize = params['fontsize'] ? params['fontsize'] : '12'
12
+ graph = (directed? ? DOT::Digraph : DOT::Graph).new(params)
13
+ edge_class = directed? ? DOT::DirectedEdge : DOT::Edge
14
+ vertex_options = params['vertex'] || {}
15
+ edge_options = params['edge'] || {}
16
+
17
+ each_vertex do |v|
18
+ default_vertex_options = {
19
+ 'name' => vertex_id(v),
20
+ 'fontsize' => fontsize,
21
+ 'label' => vertex_label(v),
22
+ 'style' => 'filled',
23
+ }
24
+ if highlight_nodes.include?(v)
25
+ default_vertex_options = default_vertex_options.merge({
26
+ 'color' => 'red',
27
+ 'fillcolor' => 'red'
28
+ })
29
+ else
30
+ default_vertex_options = default_vertex_options.merge({
31
+ 'color' => 'blue',
32
+ 'fillcolor' => 'blue'
33
+ })
34
+ end
35
+
36
+ each_vertex_options = default_vertex_options.merge(vertex_options)
37
+ vertex_options.each{|option, val| each_vertex_options[option] = val.call(v) if val.is_a?(Proc)}
38
+ graph << DOT::Node.new(each_vertex_options)
39
+ end
40
+
41
+ each_edge do |u, v|
42
+ default_edge_options = {
43
+ 'from' => vertex_id(u),
44
+ 'to' => vertex_id(v),
45
+ 'fontsize' => fontsize
46
+ }
47
+ each_edge_options = default_edge_options.merge(edge_options)
48
+ edge_options.each{|option, val| each_edge_options[option] = val.call(u, v) if val.is_a?(Proc)}
49
+ graph << edge_class.new(each_edge_options)
50
+ end
51
+
52
+ graph
53
+ end
54
+
55
+ def write_to_graphic_file(fmt='png', dotfile="graph", options={}, highlight_nodes)
56
+ src = dotfile + ".dot"
57
+ dot = dotfile + "." + fmt
58
+
59
+ File.open(src, 'w') do |f|
60
+ f << self.to_dot_graph(params=options, highlight_nodes=highlight_nodes).to_s << "\n"
61
+ end
62
+
63
+ unless system("dot -T#{fmt} #{src} -o #{dot}")
64
+ message = <<-HEREDOC # Use <<- to indent End of String terminator
65
+ Error executing dot. Did you install GraphViz?
66
+ Try installing it via Homebrew: `brew install graphviz`.
67
+ Visit https://graphviz.org/download/ for more installation instructions.
68
+ HEREDOC
69
+ raise message
70
+ end
71
+ dot
72
+ end
73
+ end
74
+ end