vanagon 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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