librarian 0.0.26 → 0.1.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +0 -3
  2. data/.travis.yml +7 -0
  3. data/Gemfile +1 -1
  4. data/{MIT-LICENSE → LICENSE.txt} +2 -0
  5. data/README.md +13 -363
  6. data/lib/librarian/chef/version.rb +5 -0
  7. data/lib/librarian/cli.rb +1 -0
  8. data/lib/librarian/environment.rb +8 -2
  9. data/lib/librarian/mock/environment.rb +6 -1
  10. data/lib/librarian/mock/version.rb +5 -0
  11. data/lib/librarian/rspec/support/cli_macro.rb +120 -0
  12. data/lib/librarian/source/git.rb +1 -1
  13. data/lib/librarian/source/git/repository.rb +10 -2
  14. data/lib/librarian/version.rb +1 -1
  15. data/librarian.gemspec +15 -22
  16. data/spec/functional/cli_spec.rb +27 -0
  17. data/spec/functional/source/git/repository_spec.rb +2 -0
  18. metadata +69 -100
  19. data/lib/librarian/chef.rb +0 -1
  20. data/lib/librarian/chef/cli.rb +0 -47
  21. data/lib/librarian/chef/dsl.rb +0 -16
  22. data/lib/librarian/chef/environment.rb +0 -27
  23. data/lib/librarian/chef/extension.rb +0 -9
  24. data/lib/librarian/chef/integration/knife.rb +0 -46
  25. data/lib/librarian/chef/manifest_reader.rb +0 -59
  26. data/lib/librarian/chef/source.rb +0 -4
  27. data/lib/librarian/chef/source/git.rb +0 -25
  28. data/lib/librarian/chef/source/github.rb +0 -27
  29. data/lib/librarian/chef/source/local.rb +0 -69
  30. data/lib/librarian/chef/source/path.rb +0 -12
  31. data/lib/librarian/chef/source/site.rb +0 -442
  32. data/lib/librarian/chef/templates/Cheffile +0 -15
  33. data/spec/functional/chef/cli_spec.rb +0 -194
  34. data/spec/functional/chef/source/site_spec.rb +0 -266
  35. data/spec/integration/chef/source/git_spec.rb +0 -441
  36. data/spec/integration/chef/source/site_spec.rb +0 -217
  37. data/spec/support/cli_macro.rb +0 -114
@@ -1 +0,0 @@
1
- require 'librarian/chef/extension'
@@ -1,47 +0,0 @@
1
- require 'librarian/helpers'
2
-
3
- require 'librarian/cli'
4
- require 'librarian/chef'
5
-
6
- module Librarian
7
- module Chef
8
- class Cli < Librarian::Cli
9
-
10
- module Particularity
11
- def root_module
12
- Chef
13
- end
14
- end
15
-
16
- extend Particularity
17
-
18
- source_root Pathname.new(__FILE__).dirname.join("templates")
19
-
20
- def init
21
- copy_file environment.specfile_name
22
- end
23
-
24
- desc "install", "Resolves and installs all of the dependencies you specify."
25
- option "quiet", :type => :boolean, :default => false
26
- option "verbose", :type => :boolean, :default => false
27
- option "line-numbers", :type => :boolean, :default => false
28
- option "clean", :type => :boolean, :default => false
29
- option "strip-dot-git", :type => :boolean
30
- option "path", :type => :string
31
- def install
32
- ensure!
33
- clean! if options["clean"]
34
- if options.include?("strip-dot-git")
35
- strip_dot_git_val = options["strip-dot-git"] ? "1" : nil
36
- environment.config_db.local["install.strip-dot-git"] = strip_dot_git_val
37
- end
38
- if options.include?("path")
39
- environment.config_db.local["path"] = options["path"]
40
- end
41
- resolve!
42
- install!
43
- end
44
-
45
- end
46
- end
47
- end
@@ -1,16 +0,0 @@
1
- require 'librarian/dsl'
2
- require 'librarian/chef/source'
3
-
4
- module Librarian
5
- module Chef
6
- class Dsl < Librarian::Dsl
7
-
8
- dependency :cookbook
9
-
10
- source :site => Source::Site
11
- source :git => Source::Git
12
- source :github => Source::Github
13
- source :path => Source::Path
14
- end
15
- end
16
- end
@@ -1,27 +0,0 @@
1
- require "librarian/environment"
2
- require "librarian/chef/dsl"
3
- require "librarian/chef/source"
4
-
5
- module Librarian
6
- module Chef
7
- class Environment < Environment
8
-
9
- def adapter_name
10
- "chef"
11
- end
12
-
13
- def install_path
14
- part = config_db["path"] || "cookbooks"
15
- project_path.join(part)
16
- end
17
-
18
- def config_keys
19
- super + %w[
20
- install.strip-dot-git
21
- path
22
- ]
23
- end
24
-
25
- end
26
- end
27
- end
@@ -1,9 +0,0 @@
1
- require 'librarian/chef/environment'
2
-
3
- module Librarian
4
- module Chef
5
- extend self
6
- extend Librarian
7
-
8
- end
9
- end
@@ -1,46 +0,0 @@
1
- require 'pathname'
2
- require 'securerandom'
3
- require 'highline'
4
-
5
- require 'librarian'
6
- require 'librarian/action/install'
7
- require 'librarian/chef'
8
-
9
- module Librarian
10
- module Chef
11
-
12
- class Environment
13
- def install_path
14
- @install_path ||= begin
15
- has_home = ENV["HOME"] && File.directory?(ENV["HOME"])
16
- tmp_dir = Pathname.new(has_home ? "~/.librarian/tmp" : "/tmp/librarian").expand_path
17
- enclosing = tmp_dir.join("chef/integration/knife/install")
18
- enclosing.mkpath unless enclosing.exist?
19
- dir = enclosing.join(SecureRandom.hex(16))
20
- dir.mkpath
21
- at_exit { dir.rmtree }
22
- dir
23
- end
24
- end
25
- end
26
-
27
- def environment
28
- @environment ||= environment_class.new
29
- end
30
-
31
- def install_path
32
- environment.install_path
33
- end
34
-
35
- hl = HighLine.new
36
-
37
- begin
38
- Action::Install.new(environment).run
39
- rescue Error => e
40
- message = hl.color(e.message, HighLine::RED)
41
- hl.say(message)
42
- Process.exit!(1)
43
- end
44
-
45
- end
46
- end
@@ -1,59 +0,0 @@
1
- require 'json'
2
- require 'yaml'
3
-
4
- require 'librarian/manifest'
5
-
6
- module Librarian
7
- module Chef
8
- module ManifestReader
9
- extend self
10
-
11
- MANIFESTS = %w(metadata.json metadata.yml metadata.yaml metadata.rb)
12
-
13
- def manifest_path(path)
14
- MANIFESTS.map{|s| path.join(s)}.find{|s| s.exist?}
15
- end
16
-
17
- def read_manifest(name, manifest_path)
18
- case manifest_path.extname
19
- when ".json" then JSON.parse(binread(manifest_path))
20
- when ".yml", ".yaml" then YAML.load(binread(manifest_path))
21
- when ".rb" then compile_manifest(name, manifest_path.dirname)
22
- end
23
- end
24
-
25
- def compile_manifest(name, path)
26
- # Inefficient, if there are many cookbooks with uncompiled metadata.
27
- require 'chef/json_compat'
28
- require 'chef/cookbook/metadata'
29
- md = ::Chef::Cookbook::Metadata.new
30
- md.name(name)
31
- md.from_file(path.join('metadata.rb').to_s)
32
- {"name" => md.name, "version" => md.version, "dependencies" => md.dependencies}
33
- end
34
-
35
- def manifest?(name, path)
36
- path = Pathname.new(path)
37
- !!manifest_path(path)
38
- end
39
-
40
- def check_manifest(name, manifest_path)
41
- manifest = read_manifest(name, manifest_path)
42
- manifest["name"] == name
43
- end
44
-
45
- private
46
-
47
- if IO.respond_to?(:binread)
48
- def binread(path)
49
- path.binread
50
- end
51
- else
52
- def binread(path)
53
- path.read
54
- end
55
- end
56
-
57
- end
58
- end
59
- end
@@ -1,4 +0,0 @@
1
- require 'librarian/chef/source/path'
2
- require 'librarian/chef/source/git'
3
- require 'librarian/chef/source/github'
4
- require 'librarian/chef/source/site'
@@ -1,25 +0,0 @@
1
- require 'librarian/source/git'
2
- require 'librarian/chef/source/local'
3
-
4
- module Librarian
5
- module Chef
6
- module Source
7
- class Git < Librarian::Source::Git
8
- include Local
9
-
10
- private
11
-
12
- def install_perform_step_copy!(found_path, install_path)
13
- debug { "Copying #{relative_path_to(found_path)} to #{relative_path_to(install_path)}" }
14
- FileUtils.cp_r(found_path, install_path)
15
-
16
- if environment.config_db["install.strip-dot-git"] == "1"
17
- dot_git = install_path.join(".git")
18
- dot_git.rmtree if dot_git.directory?
19
- end
20
- end
21
-
22
- end
23
- end
24
- end
25
- end
@@ -1,27 +0,0 @@
1
- require 'librarian/chef/source/git'
2
-
3
- module Librarian
4
- module Chef
5
- module Source
6
- class Github
7
-
8
- class << self
9
-
10
- def lock_name
11
- Git.lock_name
12
- end
13
-
14
- def from_lock_options(environment, options)
15
- Git.from_lock_options(environment, options)
16
- end
17
-
18
- def from_spec_args(environment, uri, options)
19
- Git.from_spec_args(environment, "https://github.com/#{uri}", options)
20
- end
21
-
22
- end
23
-
24
- end
25
- end
26
- end
27
- end
@@ -1,69 +0,0 @@
1
- require 'librarian/chef/manifest_reader'
2
-
3
- module Librarian
4
- module Chef
5
- module Source
6
- module Local
7
-
8
- def install!(manifest)
9
- manifest.source == self or raise ArgumentError
10
-
11
- info { "Installing #{manifest.name} (#{manifest.version})" }
12
-
13
- debug { "Installing #{manifest}" }
14
-
15
- name, version = manifest.name, manifest.version
16
- found_path = found_path(name)
17
-
18
- install_path = environment.install_path.join(name)
19
- if install_path.exist?
20
- debug { "Deleting #{relative_path_to(install_path)}" }
21
- install_path.rmtree
22
- end
23
-
24
- install_perform_step_copy!(found_path, install_path)
25
- end
26
-
27
- def fetch_version(name, extra)
28
- manifest_data(name)["version"]
29
- end
30
-
31
- def fetch_dependencies(name, version, extra)
32
- manifest_data(name)["dependencies"]
33
- end
34
-
35
- private
36
-
37
- def install_perform_step_copy!(found_path, install_path)
38
- debug { "Copying #{relative_path_to(found_path)} to #{relative_path_to(install_path)}" }
39
- FileUtils.cp_r(found_path, install_path)
40
- end
41
-
42
- def manifest_data(name)
43
- @manifest_data ||= { }
44
- @manifest_data[name] ||= fetch_manifest_data(name)
45
- end
46
-
47
- def fetch_manifest_data(name)
48
- expect_manifest!(name)
49
-
50
- found_path = found_path(name)
51
- manifest_path = ManifestReader.manifest_path(found_path)
52
- ManifestReader.read_manifest(name, manifest_path)
53
- end
54
-
55
- def manifest?(name, path)
56
- ManifestReader.manifest?(name, path)
57
- end
58
-
59
- def expect_manifest!(name)
60
- found_path = found_path(name)
61
- return if found_path && ManifestReader.manifest_path(found_path)
62
-
63
- raise Error, "No metadata file found for #{name} from #{self}! If this should be a cookbook, you might consider contributing a metadata file upstream or forking the cookbook to add your own metadata file."
64
- end
65
-
66
- end
67
- end
68
- end
69
- end
@@ -1,12 +0,0 @@
1
- require 'librarian/source/path'
2
- require 'librarian/chef/source/local'
3
-
4
- module Librarian
5
- module Chef
6
- module Source
7
- class Path < Librarian::Source::Path
8
- include Local
9
- end
10
- end
11
- end
12
- end
@@ -1,442 +0,0 @@
1
- require 'fileutils'
2
- require 'pathname'
3
- require 'uri'
4
- require 'net/http'
5
- require 'json'
6
- require 'digest'
7
- require 'zlib'
8
- require 'securerandom'
9
- require 'archive/tar/minitar'
10
-
11
- require 'librarian/source/basic_api'
12
- require 'librarian/chef/manifest_reader'
13
-
14
- module Librarian
15
- module Chef
16
- module Source
17
- class Site
18
-
19
- class Line
20
-
21
- attr_accessor :source, :name
22
- private :source=, :name=
23
-
24
- def initialize(source, name)
25
- self.source = source
26
- self.name = name
27
- end
28
-
29
- def install_version!(version, install_path)
30
- cache_version_unpacked! version
31
-
32
- if install_path.exist?
33
- debug { "Deleting #{relative_path_to(install_path)}" }
34
- install_path.rmtree
35
- end
36
-
37
- unpacked_path = version_unpacked_cache_path(version)
38
-
39
- debug { "Copying #{relative_path_to(unpacked_path)} to #{relative_path_to(install_path)}" }
40
- FileUtils.cp_r(unpacked_path, install_path)
41
- end
42
-
43
- def manifests
44
- version_uris.map do |version_uri|
45
- Manifest.new(source, name, version_uri)
46
- end
47
- end
48
-
49
- def to_version(version_uri)
50
- version_uri_metadata(version_uri)["version"]
51
- end
52
-
53
- def version_dependencies(version)
54
- version_manifest(version)["dependencies"]
55
- end
56
-
57
- private
58
-
59
- attr_accessor :metadata_cached
60
- alias metadata_cached? metadata_cached
61
-
62
- def environment
63
- source.environment
64
- end
65
-
66
- def uri
67
- @uri ||= URI.parse("#{source.uri}/cookbooks/#{name}")
68
- end
69
-
70
- def version_uris
71
- metadata["versions"]
72
- end
73
-
74
- def version_metadata(version)
75
- version_uri = to_version_uri(version)
76
- version_uri_metadata(version_uri)
77
- end
78
-
79
- def version_uri_metadata(version_uri)
80
- memo(__method__, version_uri.to_s) do
81
- cache_version_uri_metadata! version_uri
82
- parse_local_json(version_uri_metadata_cache_path(version_uri))
83
- end
84
- end
85
-
86
- def version_manifest(version)
87
- version_uri = to_version_uri(version)
88
- version_uri_manifest(version_uri)
89
- end
90
-
91
- def version_uri_manifest(version_uri)
92
- memo(__method__, version_uri.to_s) do
93
- cache_version_uri_unpacked! version_uri
94
- unpacked_path = version_uri_unpacked_cache_path(version_uri)
95
- manifest_path = ManifestReader.manifest_path(unpacked_path)
96
- ManifestReader.read_manifest(name, manifest_path)
97
- end
98
- end
99
-
100
- def metadata
101
- @metadata ||= begin
102
- cache_metadata!
103
- parse_local_json(metadata_cache_path)
104
- end
105
- end
106
-
107
- def to_version_uri(version)
108
- memo(__method__, version.to_s) do
109
- cache_version! version
110
- version_cache_path(version).read
111
- end
112
- end
113
-
114
- def metadata_cached!
115
- self.metadata_cached = true
116
- end
117
-
118
- def cache_path
119
- @cache_path ||= source.cache_path.join(name)
120
- end
121
-
122
- def metadata_cache_path
123
- @metadata_cache_path ||= cache_path.join("metadata.json")
124
- end
125
-
126
- def version_cache_path(version)
127
- memo(__method__, version.to_s) do
128
- cache_path.join("version").join(version.to_s)
129
- end
130
- end
131
-
132
- def version_uri_cache_path(version_uri)
133
- memo(__method__, version_uri.to_s) do
134
- cache_path.join("version-uri").join(hexdigest(version_uri))
135
- end
136
- end
137
-
138
- def version_metadata_cache_path(version)
139
- version_uri = to_version_uri(version)
140
- version_uri_metadata_cache_path(version_uri)
141
- end
142
-
143
- def version_uri_metadata_cache_path(version_uri)
144
- memo(__method__, version_uri.to_s) do
145
- version_uri_cache_path(version_uri).join("metadata.json")
146
- end
147
- end
148
-
149
- def version_package_cache_path(version)
150
- version_uri = to_version_uri(version)
151
- version_uri_package_cache_path(version_uri)
152
- end
153
-
154
- def version_uri_package_cache_path(version_uri)
155
- memo(__method__, version_uri.to_s) do
156
- version_uri_cache_path(version_uri).join("package.tar.gz")
157
- end
158
- end
159
-
160
- def version_unpacked_cache_path(version)
161
- version_uri = to_version_uri(version)
162
- version_uri_unpacked_cache_path(version_uri)
163
- end
164
-
165
- def version_uri_unpacked_cache_path(version_uri)
166
- memo(__method__, version_uri.to_s) do
167
- version_uri_cache_path(version_uri).join("package")
168
- end
169
- end
170
-
171
- def cache_metadata!
172
- metadata_cached? and return or metadata_cached!
173
- cache_remote_json! metadata_cache_path, uri
174
- end
175
-
176
- def cache_version_uri_metadata!(version_uri)
177
- path = version_uri_metadata_cache_path(version_uri)
178
- path.file? and return
179
-
180
- cache_remote_json! path, version_uri
181
- end
182
-
183
- def cache_version!(version)
184
- path = version_cache_path(version)
185
- path.file? and return
186
-
187
- version_uris.each do |version_uri|
188
- m = version_uri_metadata(version_uri)
189
- v = m["version"]
190
- if version.to_s == v
191
- write! path, version_uri.to_s
192
- break
193
- end
194
- end
195
- end
196
-
197
- def cache_version_package!(version)
198
- version_uri = to_version_uri(version)
199
- cache_version_uri_package! version_uri
200
- end
201
-
202
- def cache_version_uri_package!(version_uri)
203
- path = version_uri_package_cache_path(version_uri)
204
- path.file? and return
205
-
206
- file_uri = version_uri_metadata(version_uri)["file"]
207
- cache_remote_object! path, file_uri
208
- end
209
-
210
- def cache_version_unpacked!(version)
211
- version_uri = to_version_uri(version)
212
- cache_version_uri_unpacked! version_uri
213
- end
214
-
215
- def cache_version_uri_unpacked!(version_uri)
216
- cache_version_uri_package!(version_uri)
217
-
218
- path = version_uri_unpacked_cache_path(version_uri)
219
- path.directory? and return
220
-
221
- package_path = version_uri_package_cache_path(version_uri)
222
- unpacked_path = version_uri_unpacked_cache_path(version_uri)
223
-
224
- unpack_package! unpacked_path, package_path
225
- end
226
-
227
- def cache_remote_json!(path, uri)
228
- cache_remote_object!(path, uri, :type => :json)
229
- end
230
-
231
- def cache_remote_object!(path, uri, options = { })
232
- path = Pathname(path)
233
- uri = to_uri(uri)
234
- type = options[:type]
235
-
236
- debug { "Caching #{uri} to #{path}" }
237
-
238
- response = http_get(uri)
239
-
240
- object = response.body
241
- case type
242
- when :json
243
- JSON.parse(object) # verify that it's really JSON.
244
- end
245
- write! path, object
246
- end
247
-
248
- def write!(path, bytes)
249
- path.dirname.mkpath
250
- path.open("wb"){|f| f.write(bytes)}
251
- end
252
-
253
- def unpack_package!(path, source)
254
- path = Pathname(path)
255
- source = Pathname(source)
256
-
257
- temp = environment.scratch_path.join(SecureRandom.hex(16))
258
- temp.mkpath
259
-
260
- debug { "Unpacking #{relative_path_to(source)} to #{relative_path_to(temp)}" }
261
- Zlib::GzipReader.open(source) do |input|
262
- Archive::Tar::Minitar.unpack(input, temp.to_s)
263
- end
264
-
265
- # Cookbook files, as pulled from Opscode Community Site API, are
266
- # embedded in a subdirectory of the tarball. If created by git archive they
267
- # can include the subfolder `pax_global_header`, which is ignored.
268
- subtemps = temp.children
269
- subtemps.empty? and raise "The package archive was empty!"
270
- subtemps.delete_if{|pth| pth.to_s[/pax_global_header/]}
271
- subtemps.size > 1 and raise "The package archive has too many children!"
272
- subtemp = subtemps.first
273
- debug { "Moving #{relative_path_to(subtemp)} to #{relative_path_to(path)}" }
274
- FileUtils.mv(subtemp, path)
275
- ensure
276
- temp.rmtree if temp && temp.exist?
277
- end
278
-
279
- def parse_local_json(path)
280
- JSON.parse(path.read)
281
- end
282
-
283
- def hexdigest(bytes)
284
- Digest::MD5.hexdigest(bytes)
285
- end
286
-
287
- def to_uri(uri)
288
- uri = URI(uri) unless URI === uri
289
- uri
290
- end
291
-
292
- def debug(*args, &block)
293
- environment.logger.debug(*args, &block)
294
- end
295
-
296
- def relative_path_to(path)
297
- environment.logger.relative_path_to(path)
298
- end
299
-
300
- def http(uri)
301
- environment.net_http_class(uri.host).new(uri.host, uri.port)
302
- end
303
-
304
- def http_get(uri)
305
- max_redirects = 10
306
- redirects = []
307
-
308
- loop do
309
- debug { "Performing http-get for #{uri}" }
310
- http = http(uri)
311
- request = Net::HTTP::Get.new(uri.path)
312
- response = http.start{|http| http.request(request)}
313
-
314
- case response
315
- when Net::HTTPSuccess
316
- debug { "Responded with success" }
317
- return response
318
- when Net::HTTPRedirection
319
- location = response["Location"]
320
- debug { "Responded with redirect to #{uri}" }
321
- redirects.size > max_redirects and raise Error,
322
- "Could not get #{uri} because too many redirects!"
323
- redirects.include?(location) and raise Error,
324
- "Could not get #{uri} because redirect cycle!"
325
- redirects << location
326
- uri = URI.parse(location)
327
- # continue the loop
328
- else
329
- raise Error, "Could not get #{uri} because #{response.code} #{response.message}!"
330
- end
331
- end
332
- end
333
-
334
- def memo(method, *path)
335
- ivar = "@#{method}".to_sym
336
- unless memo = instance_variable_get(ivar)
337
- memo = instance_variable_set(ivar, { })
338
- end
339
-
340
- memo.key?(path) or memo[path] = yield
341
- memo[path]
342
- end
343
-
344
- end
345
-
346
- include Librarian::Source::BasicApi
347
-
348
- lock_name 'SITE'
349
- spec_options []
350
-
351
- attr_accessor :environment, :uri
352
- private :environment=, :uri=
353
-
354
- def initialize(environment, uri, options = {})
355
- self.environment = environment
356
- self.uri = uri
357
- end
358
-
359
- def to_s
360
- uri
361
- end
362
-
363
- def ==(other)
364
- other &&
365
- self.class == other.class &&
366
- self.uri == other.uri
367
- end
368
-
369
- def to_spec_args
370
- [uri, {}]
371
- end
372
-
373
- def to_lock_options
374
- {:remote => uri}
375
- end
376
-
377
- def pinned?
378
- false
379
- end
380
-
381
- def unpin!
382
- end
383
-
384
- def install!(manifest)
385
- manifest.source == self or raise ArgumentError
386
-
387
- name = manifest.name
388
- version = manifest.version
389
- install_path = install_path(name)
390
- line = line(name)
391
-
392
- info { "Installing #{manifest.name} (#{manifest.version})" }
393
-
394
- debug { "Installing #{manifest}" }
395
-
396
- line.install_version! version, install_path
397
- end
398
-
399
- # NOTE:
400
- # Assumes the Opscode Site API responds with versions in reverse sorted order
401
- def manifests(name)
402
- line(name).manifests
403
- end
404
-
405
- def cache_path
406
- @cache_path ||= begin
407
- dir = Digest::MD5.hexdigest(uri)
408
- environment.cache_path.join("source/chef/site/#{dir}")
409
- end
410
- end
411
-
412
- def install_path(name)
413
- environment.install_path.join(name)
414
- end
415
-
416
- def fetch_version(name, version_uri)
417
- line(name).to_version(version_uri)
418
- end
419
-
420
- def fetch_dependencies(name, version, version_uri)
421
- line(name).version_dependencies(version).map{|k, v| Dependency.new(k, v, nil)}
422
- end
423
-
424
- private
425
-
426
- def line(name)
427
- @line ||= { }
428
- @line[name] ||= Line.new(self, name)
429
- end
430
-
431
- def info(*args, &block)
432
- environment.logger.info(*args, &block)
433
- end
434
-
435
- def debug(*args, &block)
436
- environment.logger.debug(*args, &block)
437
- end
438
-
439
- end
440
- end
441
- end
442
- end