cocoapods-binary-cache 0.1.1

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.
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