vanagon 0.7.1 → 0.8.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/git/basic_submodules.rb +53 -0
  3. data/lib/vanagon/component.rb +19 -13
  4. data/lib/vanagon/component/dsl.rb +6 -5
  5. data/lib/vanagon/component/source.rb +80 -53
  6. data/lib/vanagon/component/source/git.rb +129 -20
  7. data/lib/vanagon/component/source/http.rb +41 -73
  8. data/lib/vanagon/component/source/local.rb +105 -62
  9. data/lib/vanagon/platform/deb.rb +8 -0
  10. data/lib/vanagon/platform/rpm.rb +4 -0
  11. data/lib/vanagon/project/dsl.rb +3 -1
  12. data/lib/vanagon/utilities.rb +11 -54
  13. data/spec/fixtures/files/fake_file_ext.7z +0 -0
  14. data/spec/fixtures/files/fake_file_ext.bz +0 -0
  15. data/spec/fixtures/files/fake_file_ext.bz2 +0 -0
  16. data/spec/fixtures/files/fake_file_ext.cpio +0 -0
  17. data/spec/fixtures/files/fake_file_ext.gz +0 -0
  18. data/spec/fixtures/files/fake_file_ext.rar +0 -0
  19. data/spec/fixtures/files/fake_file_ext.tar +0 -0
  20. data/spec/fixtures/files/fake_file_ext.tar.bz2 +0 -0
  21. data/spec/fixtures/files/fake_file_ext.tar.xz +0 -0
  22. data/spec/fixtures/files/fake_file_ext.tbz +0 -0
  23. data/spec/fixtures/files/fake_file_ext.tbz2 +0 -0
  24. data/spec/fixtures/files/fake_file_ext.txz +0 -0
  25. data/spec/fixtures/files/fake_file_ext.xz +0 -0
  26. data/spec/fixtures/files/fake_file_ext.z +0 -0
  27. data/spec/lib/vanagon/component/source/git_spec.rb +25 -17
  28. data/spec/lib/vanagon/component/source/http_spec.rb +2 -24
  29. data/spec/lib/vanagon/component/source/{localsource_spec.rb → local_spec.rb} +8 -8
  30. data/spec/lib/vanagon/component/source_spec.rb +150 -67
  31. data/spec/lib/vanagon/platform_spec.rb +40 -21
  32. data/spec/lib/vanagon/project/dsl_spec.rb +28 -3
  33. data/spec/lib/vanagon/utilities_spec.rb +0 -44
  34. metadata +28 -13
@@ -1,16 +1,41 @@
1
1
  require 'vanagon/utilities'
2
+ require 'vanagon/component/source/local'
2
3
  require 'net/http'
3
4
  require 'uri'
4
5
 
5
6
  class Vanagon
6
7
  class Component
7
8
  class Source
8
- class Http
9
+ class Http < Vanagon::Component::Source::Local
9
10
  include Vanagon::Utilities
10
- attr_accessor :url, :sum, :file, :extension, :workdir, :cleanup
11
11
 
12
- # Extensions for files we intend to unpack during the build
13
- ARCHIVE_EXTENSIONS = ['.tar.gz', '.tgz', '.zip'].freeze
12
+ # Accessors :url, :file, :extension, :workdir, :cleanup are inherited from Local
13
+ attr_accessor :sum
14
+
15
+ class << self
16
+ def valid_url?(target_url) # rubocop:disable Metrics/AbcSize
17
+ uri = URI.parse(target_url.to_s)
18
+ return false unless ['http', 'https'].include? uri.scheme
19
+
20
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
21
+ http.request(Net::HTTP::Head.new(uri)) do |response|
22
+ case response
23
+ when Net::HTTPRedirection
24
+ # By parsing the location header, we get either an absolute
25
+ # URI or a URI with a relative `path`. Adding it to `uri`
26
+ # should correctly update the relative `path` or overwrite
27
+ # the entire URI if it's absolute.
28
+ location = URI.parse(response.header['location'])
29
+ valid_url?(uri + location)
30
+ when Net::HTTPSuccess
31
+ return true
32
+ else
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
14
39
 
15
40
  # Constructor for the Http source type
16
41
  #
@@ -18,7 +43,7 @@ class Vanagon
18
43
  # @param sum [String] sum to verify the download against
19
44
  # @param workdir [String] working directory to download into
20
45
  # @raise [RuntimeError] an exception is raised is sum is nil
21
- def initialize(url, sum, workdir)
46
+ def initialize(url, sum:, workdir:, **options)
22
47
  unless sum
23
48
  fail "sum is required to validate the http source"
24
49
  end
@@ -31,31 +56,34 @@ class Vanagon
31
56
  # file as @file and the @extension for the file as a side effect.
32
57
  def fetch
33
58
  @file = download(@url)
34
- @extension = get_extension
59
+ end
60
+
61
+ def file
62
+ @file ||= fetch
35
63
  end
36
64
 
37
65
  # Verify the downloaded file matches the provided sum
38
66
  #
39
67
  # @raise [RuntimeError] an exception is raised if the sum does not match the sum of the file
40
68
  def verify
41
- puts "Verifying file: #{@file} against sum: '#{@sum}'"
42
- actual = get_md5sum(File.join(@workdir, @file))
43
- unless @sum == actual
44
- fail "Unable to verify '#{@file}'. Expected: '#{@sum}', got: '#{actual}'"
45
- end
69
+ puts "Verifying file: #{file} against sum: '#{sum}'"
70
+ actual = get_md5sum(File.join(workdir, file))
71
+ return true if sum == actual
72
+
73
+ fail "Unable to verify '#{File.join(workdir, file)}': md5sum mismatch (expected '#{sum}', got '#{actual}'')"
46
74
  end
47
75
 
48
76
  # Downloads the file from @url into the @workdir
49
77
  # @param target_url [String, URI, Addressable::URI] url of an http source to retrieve with GET
50
78
  # @raise [RuntimeError, Vanagon::Error] an exception is raised if the URI scheme cannot be handled
51
- def download(target_url, target_file = nil) # rubocop:disable Metrics/AbcSize
79
+ def download(target_url, target_file = nil, headers = { "Accept-Encoding" => "identity" }) # rubocop:disable Metrics/AbcSize
52
80
  uri = URI.parse(target_url.to_s)
53
81
  target_file ||= File.basename(uri.path)
54
82
 
55
83
  puts "Downloading file '#{target_file}' from url '#{target_url}'"
56
84
 
57
85
  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
58
- http.request(Net::HTTP::Get.new(uri)) do |response|
86
+ http.request(Net::HTTP::Get.new(uri, headers)) do |response|
59
87
  case response
60
88
  when Net::HTTPRedirection
61
89
  # By parsing the location header, we get either an absolute
@@ -81,66 +109,6 @@ class Vanagon
81
109
  Net::HTTPHeaderSyntaxError, Net::ProtocolError => e
82
110
  raise Vanagon::Error.wrap(e, "Problem downloading #{target_file} from '#{@url}'. Please verify you have the correct uri specified.")
83
111
  end
84
-
85
- # Gets the command to extract the archive given if needed (uses @extension)
86
- #
87
- # @param tar [String] the tar command to use
88
- # @return [String, nil] command to extract the source
89
- # @raise [RuntimeError] an exception is raised if there is no known extraction method for @extension
90
- def extract(tar)
91
- if ARCHIVE_EXTENSIONS.include?(@extension)
92
- case @extension
93
- when ".tar.gz", ".tgz"
94
- return "gunzip -c '#{@file}' | '#{tar}' xf -"
95
- when ".zip"
96
- return "unzip '#{@file}' || 7za x -r -tzip -o'#{File.basename(@file, '.zip')}' '#{@file}'"
97
- end
98
- else
99
- # Extension does not appear to be an archive
100
- return ':'
101
- end
102
- end
103
-
104
- # Return the correct incantation to cleanup the source archive and source directory for a given source
105
- #
106
- # @return [String] command to cleanup the source
107
- # @raise [RuntimeError] an exception is raised if there is no known extraction method for @extension
108
- def cleanup
109
- if ARCHIVE_EXTENSIONS.include?(@extension)
110
- return "rm #{@file}; rm -r #{dirname}"
111
- else
112
- # Because dirname will be ./ here, we don't want to try to nuke it
113
- return "rm #{@file}"
114
- end
115
- end
116
-
117
- # Returns the extension for @file
118
- #
119
- # @return [String] the extension of @file
120
- def get_extension
121
- extension_match = @file.match(/.*(#{Regexp.union(ARCHIVE_EXTENSIONS)})/)
122
- unless extension_match
123
- if @file.split('.').last.include?('.')
124
- return '.' + @file.split('.').last
125
- else
126
- # This is the case where the file has no extension
127
- return @file
128
- end
129
- end
130
- extension_match[1]
131
- end
132
-
133
- # The dirname to reference when building from the source
134
- #
135
- # @return [String] the directory that should be traversed into to build this source
136
- # @raise [RuntimeError] if the @extension for the @file isn't currently handled by the method
137
- def dirname
138
- if ARCHIVE_EXTENSIONS.include?(@extension)
139
- return @file.chomp(@extension)
140
- else
141
- return './'
142
- end
143
- end
144
112
  end
145
113
  end
146
114
  end
@@ -1,31 +1,52 @@
1
1
  require 'vanagon/utilities'
2
- require 'net/http'
3
- require 'uri'
4
2
 
5
3
  class Vanagon
6
4
  class Component
7
5
  class Source
8
6
  class Local
9
- include Vanagon::Utilities
10
7
  attr_accessor :url, :file, :extension, :workdir, :cleanup
11
8
 
12
9
  # Extensions for files we intend to unpack during the build
13
- ARCHIVE_EXTENSIONS = ['.tar.gz', '.tgz', '.zip'].freeze
10
+ ARCHIVE_EXTENSIONS = {
11
+ "7z" => %w(.7z),
12
+ "bzip2" => %w(.bz2 .bz),
13
+ "cpio" => %w(.cpio),
14
+ "gzip" => %w(.gz .z),
15
+ "rar" => %w(.rar),
16
+ "tar" => %w(.tar),
17
+ "tbz2" => %w(.tar.bz2 .tbz2 .tbz),
18
+ "tgz" => %w(.tar.gz .tgz),
19
+ "txz" => %w(.tar.xz .txz),
20
+ "xz" => %w(.xz),
21
+ "zip" => %w(.zip),
22
+ }.freeze
14
23
 
15
- # Constructor for the File source type
16
- #
17
- # @param url [String] url of the http source to fetch
18
- # @param workdir [String] working directory to download into
19
- def initialize(url, workdir)
20
- @url = url
21
- @workdir = workdir
24
+ class << self
25
+ def valid_file?(target_file)
26
+ File.exist?(mangle(target_file.to_s))
27
+ end
28
+
29
+ # If a scheme is specified as "file://", this will return
30
+ # strip off the scheme and delimiters -- we need to do this because
31
+ # once upon a time we allowed specifying files with no strong
32
+ # specifications for where they should be located.
33
+ def mangle(path)
34
+ path.gsub(%r{^file://}, '')
35
+ end
36
+
37
+ def archive_extensions
38
+ ARCHIVE_EXTENSIONS.values.flatten
39
+ end
22
40
  end
23
41
 
24
- # Download the source from the url specified. Sets the full path to the
25
- # file as @file and the @extension for the file as a side effect.
26
- def fetch
27
- @file = download
28
- @extension = get_extension
42
+
43
+ # Constructor for the File source type
44
+ #
45
+ # @param path [String] path of the local file to copy
46
+ # @param workdir [String] working directory to copy <path> to
47
+ def initialize(path, workdir:, **options)
48
+ @url = ::Pathname.new(mangle(path))
49
+ @workdir = ::Pathname.new(workdir)
29
50
  end
30
51
 
31
52
  # Local files need no checksum so this is a noop
@@ -36,21 +57,19 @@ class Vanagon
36
57
  # Moves file from source to workdir
37
58
  #
38
59
  # @raise [RuntimeError, Vanagon::Error] an exception is raised if the URI scheme cannot be handled
39
- def download
40
- uri = URI.parse(@url)
41
- target_file = File.basename(uri.path)
42
- puts "Moving file '#{target_file}' to workdir"
43
-
44
- uri = @url.match(/^file:\/\/(.*)$/)
45
- if uri
46
- source_file = uri[1]
47
- target_file = File.basename(source_file)
48
- FileUtils.cp(source_file, File.join(@workdir, target_file))
49
- else
50
- raise Vanagon::Error, "Unable to parse '#{@url}' for local file path."
51
- end
60
+ def copy
61
+ puts "Copying file '#{url.basename}' to workdir"
62
+
63
+ FileUtils.cp(url, file)
64
+ end
65
+ alias_method :fetch, :copy
66
+
67
+ def file
68
+ @file ||= workdir + File.basename(url)
69
+ end
52
70
 
53
- target_file
71
+ def extension
72
+ @extension ||= extname
54
73
  end
55
74
 
56
75
  # Gets the command to extract the archive given if needed (uses @extension)
@@ -58,17 +77,40 @@ class Vanagon
58
77
  # @param tar [String] the tar command to use
59
78
  # @return [String, nil] command to extract the source
60
79
  # @raise [RuntimeError] an exception is raised if there is no known extraction method for @extension
61
- def extract(tar)
62
- if ARCHIVE_EXTENSIONS.include?(@extension)
63
- case @extension
64
- when ".tar.gz", ".tgz"
65
- return "gunzip -c '#{@file}' | '#{tar}' xf -"
66
- when ".zip"
67
- return "unzip '#{@file}' || 7za x -r -tzip -o'#{File.basename(@file, '.zip')}' '#{@file}'"
68
- end
80
+ def extract(tar = "tar") # rubocop:disable Metrics/AbcSize
81
+ # Extension does not appear to be an archive, so "extract" is a no-op
82
+ return ':' unless archive_extensions.include?(extension)
83
+
84
+ case decompressor
85
+ when "7z"
86
+ %(7z x "#{file}")
87
+ when "bzip2"
88
+ %(bunzip2 "#{file}")
89
+ when "cpio"
90
+ %(
91
+ mkdir "#{file.basename}" &&
92
+ pushd "#{file.basename}" 2>&1 > /dev/null &&
93
+ cpio -idv < "#{file}" &&
94
+ popd 2>&1 > /dev/null
95
+ ).undent
96
+ when "gzip"
97
+ %(gunzip "#{file}")
98
+ when "rar"
99
+ %(unrar x "#{file}")
100
+ when "tar"
101
+ %(#{tar} xf "#{file}")
102
+ when "tbz2"
103
+ %(bunzip2 -c "#{file}" | #{tar} xf -)
104
+ when "tgz"
105
+ %(gunzip -c "#{file}" | #{tar} xf -)
106
+ when "txz"
107
+ %(unxz -d "#{file}" | #{tar} xvf -)
108
+ when "xz"
109
+ %(unxz "#{file}")
110
+ when "zip"
111
+ "unzip '#{file}' || 7za x -r -tzip -o'#{File.basename(file, '.zip')}' '#{file}'"
69
112
  else
70
- # Extension does not appear to be an archive
71
- return ':'
113
+ raise Vanagon::Error, "Don't know how to decompress #{extension} archives"
72
114
  end
73
115
  end
74
116
 
@@ -77,28 +119,28 @@ class Vanagon
77
119
  # @return [String] command to cleanup the source
78
120
  # @raise [RuntimeError] an exception is raised if there is no known extraction method for @extension
79
121
  def cleanup
80
- if ARCHIVE_EXTENSIONS.include?(@extension)
81
- return "rm #{@file}; rm -r #{dirname}"
82
- else
83
- # Because dirname will be ./ here, we don't want to try to nuke it
84
- return "rm #{@file}"
85
- end
122
+ archive? ? "rm #{file}; rm -r #{dirname}" : "rm #{file}"
86
123
  end
87
124
 
88
125
  # Returns the extension for @file
89
126
  #
90
127
  # @return [String] the extension of @file
91
- def get_extension
92
- extension_match = @file.match(/.*(#{Regexp.union(ARCHIVE_EXTENSIONS)})/)
93
- unless extension_match
94
- if @file.split('.').last.include?('.')
95
- return '.' + @file.split('.').last
96
- else
97
- # This is the case where the file has no extension
98
- return @file
99
- end
100
- end
101
- extension_match[1]
128
+ def extname
129
+ extension_match = file.to_s.match %r{#{Regexp.union(archive_extensions)}\Z}
130
+ return extension_match.to_s if extension_match
131
+ File.extname(file)
132
+ end
133
+
134
+ def archive_extensions
135
+ self.class.archive_extensions
136
+ end
137
+
138
+ def archive?
139
+ archive_extensions.include?(extension)
140
+ end
141
+
142
+ def decompressor
143
+ @decompressor ||= ARCHIVE_EXTENSIONS.select { |k, v| v.include? extension }.keys.first
102
144
  end
103
145
 
104
146
  # The dirname to reference when building from the source
@@ -106,11 +148,12 @@ class Vanagon
106
148
  # @return [String] the directory that should be traversed into to build this source
107
149
  # @raise [RuntimeError] if the @extension for the @file isn't currently handled by the method
108
150
  def dirname
109
- if ARCHIVE_EXTENSIONS.include?(@extension)
110
- return @file.chomp(@extension)
111
- else
112
- return './'
113
- end
151
+ archive? ? File.basename(file, extension) : './'
152
+ end
153
+
154
+ # Wrapper around the class method '.mangle'
155
+ def mangle(path)
156
+ self.class.mangle(path)
114
157
  end
115
158
  end
116
159
  end
@@ -47,6 +47,14 @@ class Vanagon
47
47
  "#{project.name}_#{project.version}-#{project.release}#{@codename}_#{project.noarch ? 'all' : @architecture}.deb"
48
48
  end
49
49
 
50
+ # Get the expected output dir for the debian packages. This allows us to
51
+ # use some standard tools to ship internally.
52
+ #
53
+ # @return [String] relative path to where debian packages should be staged
54
+ def output_dir(target_repo = "")
55
+ @output_dir ||= File.join("deb", @codename, target_repo)
56
+ end
57
+
50
58
  # Returns the string to add a target repo to the platforms' provisioning
51
59
  #
52
60
  # @param definition [URI] A URI to a deb or list file
@@ -36,6 +36,10 @@ class Vanagon
36
36
  "#{project.name}-#{project.version}-#{project.release}.#{project.noarch ? 'noarch' : @architecture}.rpm"
37
37
  end
38
38
 
39
+ def output_dir(target_repo = "products")
40
+ super
41
+ end
42
+
39
43
  def rpm_defines
40
44
  defines = %(--define '_topdir $(tempdir)/rpmbuild' )
41
45
  # RPM doesn't allow dashes in the os_name. This was added to
@@ -144,8 +144,10 @@ class Vanagon
144
144
  # and reachable from the current commit in that repository.
145
145
  #
146
146
  def version_from_git
147
- version = Vanagon::Utilities.git_version(File.expand_path("..", Vanagon::Driver.configdir))
147
+ version = Git.open(File.expand_path("..", Vanagon::Driver.configdir)).describe('HEAD', tags: true)
148
148
  @project.version = version.split('-').reject(&:empty?).join('.')
149
+ rescue Git::GitExecuteError
150
+ warn "Directory '#{dirname}' cannot be versioned by git. Maybe it hasn't been tagged yet?"
149
151
  end
150
152
 
151
153
  # Sets the vendor for the project. Used in packaging artifacts.
@@ -5,10 +5,15 @@ require 'json'
5
5
  require 'digest'
6
6
  require 'erb'
7
7
  require 'timeout'
8
+ # This stupid library requires a capital 'E' in its name
9
+ # but it provides a wealth of useful constants
10
+ require 'English'
8
11
  require 'vanagon/extensions/string'
9
12
 
10
13
  class Vanagon
11
14
  module Utilities
15
+ extend self
16
+
12
17
  # Utility to get the md5 sum of a file
13
18
  #
14
19
  # @param file [String] file to md5sum
@@ -93,7 +98,7 @@ class Vanagon
93
98
  # @raise [Vanagon::Error] If the command fails an exception is raised
94
99
  def ex(command)
95
100
  ret = %x(#{command})
96
- unless $?.success?
101
+ unless $CHILD_STATUS.success?
97
102
  raise Vanagon::Error, "'#{command}' did not succeed"
98
103
  end
99
104
  ret
@@ -119,6 +124,7 @@ class Vanagon
119
124
  return false
120
125
  end
121
126
  end
127
+ alias_method :which, :find_program_on_path
122
128
 
123
129
  # Method to retry a ruby block and fail if the command does not succeed
124
130
  # within the number of tries and timeout.
@@ -148,55 +154,6 @@ class Vanagon
148
154
  raise Vanagon::Error, "Block failed maximum number of #{tries} tries"
149
155
  end
150
156
 
151
- # Simple wrapper around git command line executes the given commands and
152
- # returns the results.
153
- #
154
- # @param command_string [String] The commands to be run
155
- # @param raise_error [boolean] if this function should raise an error
156
- # on a git failure
157
- # @return [String] The output of the command
158
- def git(command_string, raise_error = false)
159
- git_bin = find_program_on_path('git')
160
- output = %x(#{git_bin} #{command_string})
161
- if raise_error
162
- unless $?.success?
163
- raise %(git #{command_string} failed)
164
- end
165
- end
166
- return output
167
- end
168
-
169
- # Determines if the given directory is a git repo or not
170
- #
171
- # @param directory [String] The directory to check
172
- # @return [true, false] True if the directory is a git repo, false otherwise
173
- def is_git_repo?(directory = Dir.pwd)
174
- Dir.chdir(directory) do
175
- git('rev-parse --git-dir > /dev/null 2>&1')
176
- $?.success?
177
- end
178
- end
179
-
180
- # Determines a version for the given directory based on the git describe
181
- # for the repository
182
- #
183
- # @param directory [String] The directory to use in versioning
184
- # @return [String] The version of the directory accoring to git describe
185
- # @raise [RuntimeError] If the given directory is not a git repo
186
- def git_version(directory = Dir.pwd)
187
- if is_git_repo?(directory)
188
- Dir.chdir(directory) do
189
- version = git('describe --tags 2> /dev/null').chomp
190
- if version.empty?
191
- warn "Directory '#{directory}' cannot be versioned by git. Maybe it hasn't been tagged yet?"
192
- end
193
- return version
194
- end
195
- else
196
- fail "Directory '#{directory}' is not a git repo, cannot get a version"
197
- end
198
- end
199
-
200
157
  # Sends the desired file/directory to the destination using rsync
201
158
  #
202
159
  # @param source [String] file or directory to send
@@ -258,14 +215,14 @@ class Vanagon
258
215
  puts "Executing '#{command}' on '#{target}'"
259
216
  if return_command_output
260
217
  ret = %x(#{ssh_command(port)} -T #{target} '#{command.gsub("'", "'\\\\''")}').chomp
261
- if $?.success?
218
+ if $CHILD_STATUS.success?
262
219
  return ret
263
220
  else
264
221
  raise "Remote ssh command (#{command}) failed on '#{target}'."
265
222
  end
266
223
  else
267
224
  Kernel.system("#{ssh_command(port)} -T #{target} '#{command.gsub("'", "'\\\\''")}'")
268
- $?.success? or raise "Remote ssh command (#{command}) failed on '#{target}'."
225
+ $CHILD_STATUS.success? or raise "Remote ssh command (#{command}) failed on '#{target}'."
269
226
  end
270
227
  end
271
228
 
@@ -281,14 +238,14 @@ class Vanagon
281
238
  puts "Executing '#{command}' locally"
282
239
  if return_command_output
283
240
  ret = %x(#{command}).chomp
284
- if $?.success?
241
+ if $CHILD_STATUS.success?
285
242
  return ret
286
243
  else
287
244
  raise "Local command (#{command}) failed."
288
245
  end
289
246
  else
290
247
  Kernel.system(command)
291
- $?.success? or raise "Local command (#{command}) failed."
248
+ $CHILD_STATUS.success? or raise "Local command (#{command}) failed."
292
249
  end
293
250
  end
294
251
  end