berkshelf 3.1.5 → 3.2.0
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 +4 -4
- data/CHANGELOG.md +21 -0
- data/berkshelf.gemspec +6 -5
- data/features/commands/search.feature +2 -2
- data/features/commands/vendor.feature +6 -2
- data/features/commands/verify.feature +29 -0
- data/features/config.feature +13 -48
- data/features/step_definitions/filesystem_steps.rb +2 -2
- data/features/step_definitions/gem_steps.rb +3 -1
- data/features/step_definitions/utility_steps.rb +2 -2
- data/generator_files/Vagrantfile.erb +30 -30
- data/generator_files/metadata.rb.erb +0 -1
- data/lib/berkshelf.rb +5 -2
- data/lib/berkshelf/berksfile.rb +41 -41
- data/lib/berkshelf/cli.rb +11 -1
- data/lib/berkshelf/community_rest.rb +1 -0
- data/lib/berkshelf/config.rb +18 -4
- data/lib/berkshelf/cookbook_store.rb +1 -1
- data/lib/berkshelf/downloader.rb +4 -0
- data/lib/berkshelf/errors.rb +0 -1
- data/lib/berkshelf/file_syncer.rb +134 -0
- data/lib/berkshelf/locations/base.rb +6 -1
- data/lib/berkshelf/locations/git.rb +2 -2
- data/lib/berkshelf/lockfile.rb +14 -2
- data/lib/berkshelf/uploader.rb +10 -17
- data/lib/berkshelf/validator.rb +37 -0
- data/lib/berkshelf/version.rb +1 -1
- data/lib/berkshelf/visualizer.rb +13 -6
- data/spec/spec_helper.rb +1 -1
- data/spec/support/kitchen.rb +3 -1
- data/spec/support/matchers/file_system_matchers.rb +1 -1
- data/spec/support/matchers/filepath_matchers.rb +38 -2
- data/spec/support/shared_examples/formatter.rb +7 -7
- data/spec/unit/berkshelf/berksfile_spec.rb +51 -21
- data/spec/unit/berkshelf/cached_cookbook_spec.rb +5 -5
- data/spec/unit/berkshelf/cli_spec.rb +1 -1
- data/spec/unit/berkshelf/community_rest_spec.rb +12 -12
- data/spec/unit/berkshelf/config_spec.rb +4 -4
- data/spec/unit/berkshelf/cookbook_generator_spec.rb +2 -2
- data/spec/unit/berkshelf/cookbook_store_spec.rb +6 -6
- data/spec/unit/berkshelf/core_ext/file_utils_spec.rb +3 -3
- data/spec/unit/berkshelf/core_ext/pathname_spec.rb +23 -6
- data/spec/unit/berkshelf/dependency_spec.rb +4 -4
- data/spec/unit/berkshelf/downloader_spec.rb +5 -1
- data/spec/unit/berkshelf/errors_spec.rb +1 -1
- data/spec/unit/berkshelf/file_syncer_spec.rb +206 -0
- data/spec/unit/berkshelf/init_generator_spec.rb +19 -22
- data/spec/unit/berkshelf/installer_spec.rb +6 -6
- data/spec/unit/berkshelf/locations/base_spec.rb +17 -8
- data/spec/unit/berkshelf/locations/git_spec.rb +34 -34
- data/spec/unit/berkshelf/locations/path_spec.rb +3 -3
- data/spec/unit/berkshelf/lockfile_parser_spec.rb +1 -1
- data/spec/unit/berkshelf/lockfile_spec.rb +50 -36
- data/spec/unit/berkshelf/packager_spec.rb +6 -4
- data/spec/unit/berkshelf/resolver/graph_spec.rb +3 -3
- data/spec/unit/berkshelf/resolver_spec.rb +3 -3
- data/spec/unit/berkshelf/shell_spec.rb +30 -24
- data/spec/unit/berkshelf/uploader_spec.rb +10 -36
- data/spec/unit/berkshelf/validator_spec.rb +30 -0
- data/spec/unit/berkshelf/visualizer_spec.rb +17 -2
- metadata +34 -15
- data/lib/berkshelf/mixin/dsl_eval.rb +0 -58
- data/spec/unit/berkshelf/mixin/dsl_eval_spec.rb +0 -55
data/lib/berkshelf/cli.rb
CHANGED
@@ -134,7 +134,7 @@ module Berkshelf
|
|
134
134
|
def install
|
135
135
|
if options[:path]
|
136
136
|
# TODO: Remove in Berkshelf 4.0
|
137
|
-
Berkshelf.formatter.deprecation "`berks install --path [PATH
|
137
|
+
Berkshelf.formatter.deprecation "`berks install --path [PATH]` has been replaced by `berks vendor`."
|
138
138
|
Berkshelf.formatter.deprecation "Re-run your command as `berks vendor [PATH]` or see `berks help vendor`."
|
139
139
|
exit(1)
|
140
140
|
end
|
@@ -387,6 +387,16 @@ module Berkshelf
|
|
387
387
|
berksfile.vendor(path)
|
388
388
|
end
|
389
389
|
|
390
|
+
method_option :berksfile,
|
391
|
+
type: :string,
|
392
|
+
default: nil
|
393
|
+
desc "verify", "Perform a quick validation on the contents of your resolved cookbooks"
|
394
|
+
def verify
|
395
|
+
berksfile = Berksfile.from_options(options)
|
396
|
+
berksfile.verify
|
397
|
+
Berkshelf.formatter.msg "Verified."
|
398
|
+
end
|
399
|
+
|
390
400
|
method_option :berksfile,
|
391
401
|
type: :string,
|
392
402
|
default: nil,
|
data/lib/berkshelf/config.rb
CHANGED
@@ -60,7 +60,19 @@ module Berkshelf
|
|
60
60
|
# @param [Hash] options
|
61
61
|
# @see {Buff::Config::JSON}
|
62
62
|
def initialize(path = self.class.path, options = {})
|
63
|
-
super(path, options)
|
63
|
+
super(path, options).tap do
|
64
|
+
# Deprecation
|
65
|
+
if !self.vagrant.omnibus.enabled.nil?
|
66
|
+
Berkshelf.ui.warn "`vagrant.omnibus.enabled' is deprecated and " \
|
67
|
+
"will be removed in a future release. Please remove the " \
|
68
|
+
"`enabled' attribute from your Berkshelf config."
|
69
|
+
end
|
70
|
+
if !self.vagrant.vm.box_url.nil?
|
71
|
+
Berkshelf.ui.warn "`vagrant.vm.box_url' is deprecated and " \
|
72
|
+
"will be removed in a future release. Please remove the " \
|
73
|
+
"`box_url' attribute from your Berkshelf config."
|
74
|
+
end
|
75
|
+
end
|
64
76
|
end
|
65
77
|
|
66
78
|
attribute 'chef.chef_server_url',
|
@@ -97,19 +109,21 @@ module Berkshelf
|
|
97
109
|
type: String,
|
98
110
|
default: 'chef/ubuntu-14.04',
|
99
111
|
required: true
|
112
|
+
# @todo Deprecated, remove?
|
100
113
|
attribute 'vagrant.vm.box_url',
|
101
114
|
type: String,
|
102
|
-
default:
|
103
|
-
required: true
|
115
|
+
default: nil
|
104
116
|
attribute 'vagrant.vm.forward_port',
|
105
117
|
type: Hash,
|
106
118
|
default: Hash.new
|
107
119
|
attribute 'vagrant.vm.provision',
|
108
120
|
type: String,
|
109
121
|
default: 'chef_solo'
|
122
|
+
# @todo Deprecated, remove. There's a really weird tri-state here where
|
123
|
+
# nil is used to represent an unset value, just FYI
|
110
124
|
attribute 'vagrant.omnibus.enabled',
|
111
125
|
type: Buff::Boolean,
|
112
|
-
default:
|
126
|
+
default: nil
|
113
127
|
attribute 'vagrant.omnibus.version',
|
114
128
|
type: String,
|
115
129
|
default: 'latest'
|
@@ -121,7 +121,7 @@ module Berkshelf
|
|
121
121
|
msg << "of a name attribute as a bug.\n\n"
|
122
122
|
msg << "You can remove each cookbook in #{skipped_cookbooks} from the Berkshelf shelf "
|
123
123
|
msg << "by using the `berks shelf uninstall` command:\n\n"
|
124
|
-
msg << " $
|
124
|
+
msg << " $ berks shelf uninstall <name>"
|
125
125
|
Berkshelf.log.warn msg
|
126
126
|
end
|
127
127
|
|
data/lib/berkshelf/downloader.rb
CHANGED
@@ -133,6 +133,10 @@ module Berkshelf
|
|
133
133
|
end.first
|
134
134
|
|
135
135
|
(unpack_dir + cookbook_directory).to_s
|
136
|
+
when :file_store
|
137
|
+
tmp_dir = Dir.mktmpdir
|
138
|
+
FileUtils.cp_r(remote_cookbook.location_path, tmp_dir)
|
139
|
+
File.join(tmp_dir, name)
|
136
140
|
else
|
137
141
|
raise RuntimeError, "unknown location type #{remote_cookbook.location_type}"
|
138
142
|
end
|
data/lib/berkshelf/errors.rb
CHANGED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Berkshelf
|
4
|
+
module FileSyncer
|
5
|
+
extend self
|
6
|
+
|
7
|
+
# Files to be ignored during a directory globbing
|
8
|
+
IGNORED_FILES = %w(. ..).freeze
|
9
|
+
|
10
|
+
#
|
11
|
+
# Glob across the given pattern, accounting for dotfiles, removing Ruby's
|
12
|
+
# dumb idea to include +'.'+ and +'..'+ as entries.
|
13
|
+
#
|
14
|
+
# @param [String] pattern
|
15
|
+
# the path or glob pattern to get all files from
|
16
|
+
#
|
17
|
+
# @return [Array<String>]
|
18
|
+
# the list of all files
|
19
|
+
#
|
20
|
+
def glob(pattern)
|
21
|
+
Dir.glob(pattern, File::FNM_DOTMATCH).sort.reject do |file|
|
22
|
+
basename = File.basename(file)
|
23
|
+
IGNORED_FILES.include?(basename)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Copy the files from +source+ to +destination+, while removing any files
|
29
|
+
# in +destination+ that are not present in +source+.
|
30
|
+
#
|
31
|
+
# The method accepts an optional +:exclude+ parameter to ignore files and
|
32
|
+
# folders that match the given pattern(s). Note the exclude pattern behaves
|
33
|
+
# on paths relative to the given source. If you want to exclude a nested
|
34
|
+
# directory, you will need to use something like +**/directory+.
|
35
|
+
#
|
36
|
+
# @raise ArgumentError
|
37
|
+
# if the +source+ parameter is not a directory
|
38
|
+
#
|
39
|
+
# @param [String] source
|
40
|
+
# the path on disk to sync from
|
41
|
+
# @param [String] destination
|
42
|
+
# the path on disk to sync to
|
43
|
+
#
|
44
|
+
# @option options [String, Array<String>] :exclude
|
45
|
+
# a file, folder, or globbing pattern of files to ignore when syncing
|
46
|
+
#
|
47
|
+
# @return [true]
|
48
|
+
#
|
49
|
+
def sync(source, destination, options = {})
|
50
|
+
unless File.directory?(source)
|
51
|
+
raise ArgumentError, "`source' must be a directory, but was a " \
|
52
|
+
"`#{File.ftype(source)}'! If you just want to sync a file, use " \
|
53
|
+
"the `copy' method instead."
|
54
|
+
end
|
55
|
+
|
56
|
+
# Reject any files that match the excludes pattern
|
57
|
+
excludes = Array(options[:exclude]).map do |exclude|
|
58
|
+
[exclude, "#{exclude}/*"]
|
59
|
+
end.flatten
|
60
|
+
|
61
|
+
source_files = glob(File.join(source, '**/*'))
|
62
|
+
source_files = source_files.reject do |source_file|
|
63
|
+
basename = relative_path_for(source_file, source)
|
64
|
+
excludes.any? { |exclude| File.fnmatch?(exclude, basename, File::FNM_DOTMATCH) }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Ensure the destination directory exists
|
68
|
+
FileUtils.mkdir_p(destination) unless File.directory?(destination)
|
69
|
+
|
70
|
+
# Copy over the filtered source files
|
71
|
+
source_files.each do |source_file|
|
72
|
+
relative_path = relative_path_for(source_file, source)
|
73
|
+
|
74
|
+
# Create the parent directory
|
75
|
+
parent = File.join(destination, File.dirname(relative_path))
|
76
|
+
FileUtils.mkdir_p(parent) unless File.directory?(parent)
|
77
|
+
|
78
|
+
case File.ftype(source_file).to_sym
|
79
|
+
when :directory
|
80
|
+
FileUtils.mkdir_p("#{destination}/#{relative_path}")
|
81
|
+
when :link
|
82
|
+
target = File.readlink(source_file)
|
83
|
+
|
84
|
+
Dir.chdir(destination) do
|
85
|
+
FileUtils.ln_sf(target, "#{destination}/#{relative_path}")
|
86
|
+
end
|
87
|
+
when :file
|
88
|
+
FileUtils.cp(source_file, "#{destination}/#{relative_path}")
|
89
|
+
else
|
90
|
+
type = File.ftype(source_file)
|
91
|
+
raise RuntimeError, "Unknown file type: `#{type}' at " \
|
92
|
+
"`#{source_file}'. Failed to sync `#{source_file}' to " \
|
93
|
+
"`#{destination}/#{relative_path}'!"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Remove any files in the destination that are not in the source files
|
98
|
+
destination_files = glob("#{destination}/**/*")
|
99
|
+
|
100
|
+
# Calculate the relative paths of files so we can compare to the
|
101
|
+
# source.
|
102
|
+
relative_source_files = source_files.map do |file|
|
103
|
+
relative_path_for(file, source)
|
104
|
+
end
|
105
|
+
relative_destination_files = destination_files.map do |file|
|
106
|
+
relative_path_for(file, destination)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Remove any extra files that are present in the destination, but are
|
110
|
+
# not in the source list
|
111
|
+
extra_files = relative_destination_files - relative_source_files
|
112
|
+
extra_files.each do |file|
|
113
|
+
FileUtils.rm_rf(File.join(destination, file))
|
114
|
+
end
|
115
|
+
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
#
|
121
|
+
# The relative path of the given +path+ to the +parent+.
|
122
|
+
#
|
123
|
+
# @param [String] path
|
124
|
+
# the path to get relative with
|
125
|
+
# @param [String] parent
|
126
|
+
# the parent where the path is contained (hopefully)
|
127
|
+
#
|
128
|
+
# @return [String]
|
129
|
+
#
|
130
|
+
def relative_path_for(path, parent)
|
131
|
+
Pathname.new(path).relative_path_from(Pathname.new(parent)).to_s
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -59,7 +59,12 @@ module Berkshelf
|
|
59
59
|
raise NotACookbook.new(path)
|
60
60
|
end
|
61
61
|
|
62
|
-
|
62
|
+
begin
|
63
|
+
cookbook = CachedCookbook.from_path(path)
|
64
|
+
rescue Ridley::Errors::RidleyError => e
|
65
|
+
raise InternalError, "The following error occurred while reading the " \
|
66
|
+
"cookbook `#{dependency.name}':\n#{e.class}: #{e.message}"
|
67
|
+
end
|
63
68
|
|
64
69
|
unless @dependency.version_constraint.satisfies?(cookbook.version)
|
65
70
|
raise CookbookValidationFailure.new(dependency, cookbook)
|
@@ -27,7 +27,7 @@ module Berkshelf
|
|
27
27
|
|
28
28
|
# @see BaseLoation#installed?
|
29
29
|
def installed?
|
30
|
-
revision && install_path.exist?
|
30
|
+
!!(revision && install_path.exist?)
|
31
31
|
end
|
32
32
|
|
33
33
|
# Install this git cookbook into the cookbook store. This method leverages
|
@@ -71,7 +71,7 @@ module Berkshelf
|
|
71
71
|
FileUtils.cp_r(scratch_path, install_path)
|
72
72
|
|
73
73
|
# Remove the git history
|
74
|
-
FileUtils.rm_rf(File.join(install_path, '.git'))
|
74
|
+
FileUtils.rm_rf(File.join(install_path, '.git'))
|
75
75
|
|
76
76
|
install_path.chmod(0777 & ~File.umask)
|
77
77
|
ensure
|
data/lib/berkshelf/lockfile.rb
CHANGED
@@ -16,7 +16,10 @@ module Berkshelf
|
|
16
16
|
# @param [Berkshelf::Berksfile] berksfile
|
17
17
|
# the Berksfile associated with the Lockfile
|
18
18
|
def from_berksfile(berksfile)
|
19
|
-
|
19
|
+
parent = File.expand_path(File.dirname(berksfile.filepath))
|
20
|
+
lockfile_name = "#{File.basename(berksfile.filepath)}.lock"
|
21
|
+
|
22
|
+
filepath = File.join(parent, lockfile_name)
|
20
23
|
new(berksfile: berksfile, filepath: filepath)
|
21
24
|
end
|
22
25
|
end
|
@@ -36,7 +39,7 @@ module Berkshelf
|
|
36
39
|
# the Berksfile for this Lockfile
|
37
40
|
attr_reader :berksfile
|
38
41
|
|
39
|
-
# @return [
|
42
|
+
# @return [Lockfile::Graph]
|
40
43
|
# the dependency graph
|
41
44
|
attr_reader :graph
|
42
45
|
|
@@ -213,6 +216,11 @@ module Berkshelf
|
|
213
216
|
end
|
214
217
|
end
|
215
218
|
|
219
|
+
# @return [Array<CachedCookbook>]
|
220
|
+
def cached
|
221
|
+
graph.locks.values.collect { |dependency| dependency.cached_cookbook }
|
222
|
+
end
|
223
|
+
|
216
224
|
# The list of dependencies constrained in this lockfile.
|
217
225
|
#
|
218
226
|
# @return [Array<Berkshelf::Dependency>]
|
@@ -257,6 +265,10 @@ module Berkshelf
|
|
257
265
|
@dependencies[Dependency.name(dependency)] = dependency
|
258
266
|
end
|
259
267
|
|
268
|
+
def locks
|
269
|
+
graph.locks
|
270
|
+
end
|
271
|
+
|
260
272
|
# Retrieve information about a given cookbook that is in this lockfile.
|
261
273
|
#
|
262
274
|
# @raise [DependencyNotFound]
|
data/lib/berkshelf/uploader.rb
CHANGED
@@ -31,7 +31,7 @@ module Berkshelf
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Perform all validations first to prevent partially uploaded cookbooks
|
34
|
-
|
34
|
+
Validator.validate_files(cookbooks)
|
35
35
|
|
36
36
|
upload(cookbooks)
|
37
37
|
cookbooks
|
@@ -100,23 +100,16 @@ module Berkshelf
|
|
100
100
|
cookbooks[dependency] ||= lockfile.retrieve(dependency)
|
101
101
|
end
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
path = cookbook.path.to_s
|
113
|
-
|
114
|
-
files = Dir.glob(File.join(path, '**', '*.rb')).select do |f|
|
115
|
-
parent = Pathname.new(path).dirname.to_s
|
116
|
-
f.gsub(parent, '') =~ /[[:space:]]/
|
103
|
+
# This is a temporary change and will be removed in a future release. We should
|
104
|
+
# add the ability to define a custom uploader which would allow the authors of Chef-Guard
|
105
|
+
# to define their upload strategy instead of using Ridley.
|
106
|
+
#
|
107
|
+
# See https://github.com/berkshelf/berkshelf/pull/1316 for details.
|
108
|
+
if Berkshelf.chef_config.knife[:chef_guard] == true
|
109
|
+
cookbooks.values
|
110
|
+
else
|
111
|
+
cookbooks.values.sort
|
117
112
|
end
|
118
|
-
|
119
|
-
raise InvalidCookbookFiles.new(cookbook, files) unless files.empty?
|
120
113
|
end
|
121
114
|
end
|
122
115
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Berkshelf
|
2
|
+
module Validator
|
3
|
+
class << self
|
4
|
+
# Perform a complete cookbook validation checking:
|
5
|
+
# * File names for inappropriate characters
|
6
|
+
# * Invalid Ruby syntax
|
7
|
+
# * Invalid ERB templates
|
8
|
+
#
|
9
|
+
# @param [Array<CachedCookbook>, CachedCookbook] cookbooks
|
10
|
+
# the Cookbook(s) to validate
|
11
|
+
def validate(cookbooks)
|
12
|
+
Array(cookbooks).each do |cookbook|
|
13
|
+
validate_files(cookbook)
|
14
|
+
cookbook.validate
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Validate that the given cookbook does not have "bad" files. Currently
|
19
|
+
# this means including spaces in filenames (such as recipes)
|
20
|
+
#
|
21
|
+
# @param [Array<CachedCookbook>, CachedCookbook] cookbooks
|
22
|
+
# the Cookbook(s) to validate
|
23
|
+
def validate_files(cookbooks)
|
24
|
+
Array(cookbooks).each do |cookbook|
|
25
|
+
path = cookbook.path.to_s
|
26
|
+
|
27
|
+
files = Dir.glob(File.join(path, '**', '*.rb')).select do |f|
|
28
|
+
parent = Pathname.new(path).dirname.to_s
|
29
|
+
f.gsub(parent, '') =~ /[[:space:]]/
|
30
|
+
end
|
31
|
+
|
32
|
+
raise InvalidCookbookFiles.new(cookbook, files) unless files.empty?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/berkshelf/version.rb
CHANGED
data/lib/berkshelf/visualizer.rb
CHANGED
@@ -11,7 +11,7 @@ module Berkshelf
|
|
11
11
|
instance.node(item.name)
|
12
12
|
|
13
13
|
item.dependencies.each do |name, version|
|
14
|
-
instance.edge(item.name, name)
|
14
|
+
instance.edge(item.name, name, version)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -37,11 +37,11 @@ module Berkshelf
|
|
37
37
|
nodes.each(&block)
|
38
38
|
end
|
39
39
|
|
40
|
-
def edge(a, b)
|
40
|
+
def edge(a, b, version)
|
41
41
|
node(a)
|
42
42
|
node(b)
|
43
43
|
|
44
|
-
@nodes[a].add(b)
|
44
|
+
@nodes[a].add(b => version)
|
45
45
|
end
|
46
46
|
|
47
47
|
def adjacencies(object)
|
@@ -61,7 +61,14 @@ module Berkshelf
|
|
61
61
|
|
62
62
|
nodes.each do |node|
|
63
63
|
adjacencies(node).each do |edge|
|
64
|
-
|
64
|
+
edge.each do |name, version|
|
65
|
+
if version == Semverse::DEFAULT_CONSTRAINT
|
66
|
+
label = ""
|
67
|
+
else
|
68
|
+
label = " #{version}"
|
69
|
+
end
|
70
|
+
out << %| "#{node}" -> "#{name}" [ fontsize = 10, label = "#{label}" ]\n|
|
71
|
+
end
|
65
72
|
end
|
66
73
|
end
|
67
74
|
|
@@ -81,11 +88,11 @@ module Berkshelf
|
|
81
88
|
tempfile.write(to_dot)
|
82
89
|
tempfile.rewind
|
83
90
|
|
84
|
-
unless Berkshelf.which('dot')
|
91
|
+
unless Berkshelf.which('dot') || Berkshelf.which('dot.exe')
|
85
92
|
raise GraphvizNotInstalled.new
|
86
93
|
end
|
87
94
|
|
88
|
-
command =
|
95
|
+
command = %|dot -T png #{tempfile.path} -o "#{outfile}"|
|
89
96
|
response = shell_out(command)
|
90
97
|
|
91
98
|
unless response.success?
|