omnibus 4.0.0.beta.1 → 4.0.0.rc.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/docs/Building on RHEL.md +1 -0
  4. data/lib/omnibus/builder.rb +48 -1
  5. data/lib/omnibus/config.rb +13 -0
  6. data/lib/omnibus/digestable.rb +2 -2
  7. data/lib/omnibus/exceptions.rb +21 -0
  8. data/lib/omnibus/fetchers/git_fetcher.rb +75 -9
  9. data/lib/omnibus/fetchers/net_fetcher.rb +5 -3
  10. data/lib/omnibus/generator.rb +0 -13
  11. data/{resources/bff/postinstall.sh → lib/omnibus/generator_files/package_scripts/makeselfinst.erb} +0 -0
  12. data/lib/omnibus/packagers/base.rb +25 -1
  13. data/lib/omnibus/packagers/bff.rb +99 -12
  14. data/lib/omnibus/packagers/deb.rb +10 -8
  15. data/lib/omnibus/packagers/makeself.rb +24 -16
  16. data/lib/omnibus/packagers/msi.rb +6 -5
  17. data/lib/omnibus/packagers/pkg.rb +59 -10
  18. data/lib/omnibus/packagers/rpm.rb +42 -25
  19. data/lib/omnibus/packagers/solaris.rb +5 -5
  20. data/lib/omnibus/project.rb +54 -5
  21. data/lib/omnibus/software.rb +37 -39
  22. data/lib/omnibus/version.rb +1 -1
  23. data/omnibus.gemspec +1 -0
  24. data/resources/bff/gen.template.erb +3 -2
  25. data/resources/rpm/signing.erb +1 -1
  26. data/spec/functional/builder_spec.rb +75 -3
  27. data/spec/functional/fetchers/git_fetcher_spec.rb +31 -2
  28. data/spec/support/examples.rb +8 -2
  29. data/spec/support/git_helpers.rb +8 -0
  30. data/spec/unit/builder_spec.rb +6 -0
  31. data/spec/unit/config_spec.rb +1 -0
  32. data/spec/unit/generator_spec.rb +0 -12
  33. data/spec/unit/packagers/base_spec.rb +16 -0
  34. data/spec/unit/packagers/bff_spec.rb +58 -5
  35. data/spec/unit/packagers/deb_spec.rb +15 -3
  36. data/spec/unit/packagers/makeself_spec.rb +56 -9
  37. data/spec/unit/packagers/pkg_spec.rb +57 -4
  38. data/spec/unit/packagers/rpm_spec.rb +38 -23
  39. data/spec/unit/project_spec.rb +16 -5
  40. data/spec/unit/software_spec.rb +0 -1
  41. metadata +18 -6
  42. data/resources/bff/unpostinstall.sh +0 -0
  43. data/resources/makeself/post_extract.sh.erb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dec9872ce88998ffa0792cb936e9f81a5955c5f3
4
- data.tar.gz: 114a41ab14dc94359e1efb6a8f1f6e375031438e
3
+ metadata.gz: f6839ae316c6146ee459f8acbd869d83d495d90c
4
+ data.tar.gz: 30a70eb80420dbab5e8196eb1a626b7b62c7b65f
5
5
  SHA512:
6
- metadata.gz: e3cf0b1538b35bb9750427bab3c3779d1355c491e55ac889b81f4b970e85657c2053ec0a6872fd9318e82be70561b77cb354added176c59b6e3b11d536a1dc43
7
- data.tar.gz: f551ee709ea94e53af2a6b0bc5e09c7e144a686eab51c692fdbc84f74e13b17bd588c4d2c8fc779ec9bf912784583919edadc09d2120f2f94e1554b4e504437f
6
+ metadata.gz: 4b8c99b56cd409f5b4df98dea2e19ade09fdeac89e42ee53a3724cf0a1c6440430d9994fd41b36aecd68c1c9239709bf578590b46ad1602f4fc2a6fbb21e3ba4
7
+ data.tar.gz: d07dbc0bcab759ecc4f6017d71e662cd07d81ff7919854fd790fe4ec476eacdf11f8eb8a8163d2005808d8618d947d791df6f62da8e620f5cf009647bef685f9
@@ -1,6 +1,35 @@
1
1
  Omnibus CHANGELOG
2
2
  =================
3
3
 
4
+ v4.0.0.rc.1 (September 23, 2014)
5
+ --------------------------------
6
+
7
+ ### New Features
8
+ - Expose `build_version` to all `pkg` templates
9
+ - Improve info messages during RPM creation
10
+ - Make PKG packager aware of scripts with default Omnibus naming
11
+ - Make RPM packager aware of scripts with default Omnibus naming
12
+ - Clean up script logic in BFF packager
13
+ - Add an option for configuring Fetcher read timeout
14
+
15
+ ### DSL Changes
16
+ #### Builder
17
+ - Add an `appbundle` function to the builder DSL
18
+
19
+ #### Packager
20
+ - Expose `install_dir` in Packager DSL
21
+ - Expose `windows_safe_path` in Packager DSL
22
+
23
+ ### Bugfixes
24
+ - Replace dashes with underscores in RPM version names.
25
+ - The achitecture of deb package is i386, not i686.
26
+ - Re-ignore 204 exit code from `light.exe`
27
+ - Use single quotes in main `rpmbuild` command
28
+ - Be sure to create and sign the RPM if `~/.rpmmacros` exists
29
+ - Switch to OpenSSL::Digest which is threadsafe on 2.1.2
30
+ - Ensure `GitFetcher` properly resolves remote refs
31
+ - Ensure we clean ALL Ruby environment vars
32
+
4
33
  v4.0.0.beta.1 (August 20, 2014)
5
34
  -------------------------------
6
35
  ### New Features
@@ -19,6 +19,7 @@ The following Project values are taken into consideration when building RPMs:
19
19
  - `extra_package_files`
20
20
  - `iteration`
21
21
  - `maintainer`
22
+ - `package_name`
22
23
  - `package_user`
23
24
  - `package_group`
24
25
  - `package_scripts_path`
@@ -261,6 +261,46 @@ module Omnibus
261
261
  end
262
262
  expose :bundle
263
263
 
264
+ #
265
+ # Execute the given appbundler command against the embedded Ruby's
266
+ # appbundler. This command assumes the +appbundle+ gem is installed and
267
+ # in the embedded Ruby. You should add a dependency on the +appbundler+
268
+ # software definition if you want to use this command.
269
+ #
270
+ # @example
271
+ # appbundle 'chef'
272
+ #
273
+ # @param (see #command)
274
+ # @return (see #command)
275
+ #
276
+ def appbundle(app_name, options = {})
277
+ build_commands << BuildCommand.new("appbundle `#{app_name}'") do
278
+ bin_dir = "#{install_dir}/bin"
279
+ embedded_apps_root = "#{install_dir}/embedded/apps"
280
+ embedded_app_dir = "#{embedded_apps_root}/#{app_name}"
281
+ gemfile_lock = "#{embedded_app_dir}/Gemfile.lock"
282
+ appbundler_bin = windows_safe_path("#{install_dir}/embedded/bin/appbundler")
283
+
284
+ # Ensure the main bin dir exists
285
+ FileUtils.mkdir_p(bin_dir)
286
+ # Ensure the embedded app directory exists
287
+ FileUtils.mkdir_p(embedded_apps_root)
288
+ # Copy the application code into place
289
+ FileUtils.cp_r("#{Omnibus::Config.source_dir}/#{app_name}", embedded_apps_root)
290
+ # Delete any top-level `.git` directory
291
+ FileUtils.rm_rf("#{embedded_app_dir}/.git")
292
+
293
+ # Prepare the environment
294
+ options[:env] ||= {}
295
+ env = with_embedded_path || {}
296
+ env["BUNDLE_GEMFILE"] = gemfile_lock
297
+ options[:env].merge!(env)
298
+
299
+ shellout!("#{appbundler_bin} '#{embedded_app_dir}' '#{bin_dir}'", options)
300
+ end
301
+ end
302
+ expose :appbundle
303
+
264
304
  #
265
305
  # Execute the given Rake command against the embedded Ruby's rake. This
266
306
  # command assumes the +rake+ gem has been installed.
@@ -704,11 +744,17 @@ module Omnibus
704
744
  # which only removes Bundler-specific values. We need to remove all
705
745
  # values, specifically:
706
746
  #
747
+ # - _ORIGINAL_GEM_PATH
707
748
  # - GEM_PATH
708
749
  # - GEM_HOME
709
750
  # - GEM_ROOT
751
+ # - BUNDLE_BIN_PATH
710
752
  # - BUNDLE_GEMFILE
753
+ # - RUBYLIB
711
754
  # - RUBYOPT
755
+ # - RUBY_ENGINE
756
+ # - RUBY_ROOT
757
+ # - RUBY_VERSION
712
758
  #
713
759
  # The original environment restored at the end of this call.
714
760
  #
@@ -718,9 +764,10 @@ module Omnibus
718
764
  def with_clean_env(&block)
719
765
  original = ENV.to_hash
720
766
 
721
- ENV.delete('RUBYOPT')
767
+ ENV.delete('_ORIGINAL_GEM_PATH')
722
768
  ENV.delete_if { |k,_| k.start_with?('BUNDLE_') }
723
769
  ENV.delete_if { |k,_| k.start_with?('GEM_') }
770
+ ENV.delete_if { |k,_| k.start_with?('RUBY') }
724
771
 
725
772
  block.call
726
773
  ensure
@@ -466,6 +466,19 @@ module Omnibus
466
466
  # @!endgroup
467
467
  #
468
468
 
469
+ #
470
+ # @!group Fetcher Parameters
471
+ # --------------------------------------------------
472
+
473
+ # The number of seconds to wait
474
+ #
475
+ # @return [Integer]
476
+ default(:fetcher_read_timeout, 60)
477
+
478
+ # --------------------------------------------------
479
+ # @!endgroup
480
+ #
481
+
469
482
  private
470
483
 
471
484
  #
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
- require 'digest'
17
+ require 'openssl'
18
18
  require 'pathname'
19
19
 
20
20
  module Omnibus
@@ -96,7 +96,7 @@ module Omnibus
96
96
  #
97
97
  def digest_from_type(type)
98
98
  id = type.to_s.upcase
99
- instance = Digest.const_get(id).new
99
+ instance = OpenSSL::Digest.const_get(id).new
100
100
  end
101
101
 
102
102
  #
@@ -285,6 +285,27 @@ The following shell command timed out at #{timeout} seconds:
285
285
  Please increase the `:timeout' value or run the command manually to make sure it
286
286
  is completing successfully. Sometimes it is common for a command to wait for
287
287
  user input.
288
+ EOH
289
+ end
290
+ end
291
+
292
+ class ProjectAlreadyDirty < Error
293
+ def initialize(project)
294
+ name = project.name
295
+ culprit = project.culprit.name
296
+
297
+ super <<-EOH
298
+ The project `#{name}' was already marked as dirty by `#{culprit}'. You cannot
299
+ mark a project as dirty twice. This is probably a bug in Omnibus and should be
300
+ reported.
301
+ EOH
302
+ end
303
+ end
304
+
305
+ class UnresolvableGitReference < Error
306
+ def initialize(ref)
307
+ super <<-EOH
308
+ Could not resolve `#{ref}' to a valid git SHA-1.
288
309
  EOH
289
310
  end
290
311
  end
@@ -146,15 +146,11 @@ module Omnibus
146
146
  # @return [String]
147
147
  #
148
148
  def target_revision
149
- @target_revision ||= git("rev-parse #{version}").stdout.strip
150
- rescue CommandFailed => e
151
- if e.message.include?('ambiguous argument')
152
- @target_revision = git("rev-parse origin/#{version}").stdout.strip
153
- @target_revision
154
- else
155
- log.warn { 'Could not determine target revision!' }
156
- nil
157
- end
149
+ @target_revision ||= if sha_hash?(version)
150
+ version
151
+ else
152
+ revision_from_remote_reference(version)
153
+ end
158
154
  end
159
155
 
160
156
  #
@@ -166,6 +162,76 @@ module Omnibus
166
162
  current_revision == target_revision
167
163
  end
168
164
 
165
+ #
166
+ # Determine if the given revision is a SHA
167
+ #
168
+ # @return [true, false]
169
+ #
170
+ def sha_hash?(rev)
171
+ rev =~ /^[0-9a-f]{4,40}$/
172
+ end
173
+
174
+ #
175
+ # Return the SHA corresponding to ref. If ref is an annotated tag,
176
+ # return the SHA that was tagged not the SHA of the tag itself.
177
+ #
178
+ # @return [String]
179
+ #
180
+ def revision_from_remote_reference(ref)
181
+ # execute `git ls-remote` the trailing '*' does globbing. This
182
+ # allows us to return the SHA of the tagged commit for annotated
183
+ # tags. We take care to only return exact matches in
184
+ # process_remote_list.
185
+ remote_list = git("ls-remote origin #{ref}*").stdout
186
+ commit_ref = dereference_annotated_tag(remote_list, ref)
187
+
188
+ unless commit_ref
189
+ raise UnresolvableGitReference.new(ref)
190
+ end
191
+ commit_ref
192
+ end
193
+
194
+ #
195
+ # Dereference annotated tags.
196
+ #
197
+ # The +remote_list+ parameter is assumed to look like this:
198
+ #
199
+ # a2ed66c01f42514bcab77fd628149eccb4ecee28 refs/tags/rel-0.11.0
200
+ # f915286abdbc1907878376cce9222ac0b08b12b8 refs/tags/rel-0.11.0^{}
201
+ #
202
+ # The SHA with ^{} is the commit pointed to by an annotated
203
+ # tag. If ref isn't an annotated tag, there will not be a line
204
+ # with trailing ^{}.
205
+ #
206
+ # @param [String] remote_list
207
+ # output from `git ls-remote origin` command
208
+ # @param [String] ref
209
+ # the target git ref
210
+ #
211
+ # @return [String]
212
+ #
213
+ def dereference_annotated_tag(remote_list, ref)
214
+ # We'll return the SHA corresponding to the ^{} which is the
215
+ # commit pointed to by an annotated tag. If no such commit
216
+ # exists (not an annotated tag) then we return the SHA of the
217
+ # ref. If nothing matches, return "".
218
+ lines = remote_list.split("\n")
219
+ matches = lines.map { |line| line.split("\t") }
220
+ # First try for ^{} indicating the commit pointed to by an
221
+ # annotated tag.
222
+ tagged_commit = matches.find { |m| m[1].end_with?("#{ref}^{}") }
223
+ if tagged_commit
224
+ tagged_commit.first
225
+ else
226
+ found = matches.find { |m| m[1].end_with?("#{ref}") }
227
+ if found
228
+ found.first
229
+ else
230
+ nil
231
+ end
232
+ end
233
+ end
234
+
169
235
  #
170
236
  # Execute the given git command, inside the +project_dir+.
171
237
  #
@@ -139,14 +139,16 @@ module Omnibus
139
139
  def download
140
140
  log.warn(log_key) { source[:warning] } if source.key?(:warning)
141
141
 
142
- headers = download_headers
142
+ options = download_headers
143
143
 
144
144
  if source[:unsafe]
145
145
  log.warn(log_key) { "Permitting unsafe redirects!" }
146
- headers[:allow_unsafe_redirects] = true
146
+ options[:allow_unsafe_redirects] = true
147
147
  end
148
148
 
149
- file = open(download_url, headers)
149
+ options[:read_timeout] = Omnibus::Config.fetcher_read_timeout
150
+
151
+ file = open(download_url, options)
150
152
  FileUtils.cp(file.path, downloaded_file)
151
153
  file.close
152
154
  rescue SocketError,
@@ -50,11 +50,6 @@ module Omnibus
50
50
  type: :boolean,
51
51
  default: false
52
52
 
53
- class_option :makeself_assets,
54
- desc: 'Generate makeself assets',
55
- type: :boolean,
56
- default: false
57
-
58
53
  class_option :msi_assets,
59
54
  desc: 'Generate Windows MSI assets',
60
55
  type: :boolean,
@@ -112,8 +107,6 @@ module Omnibus
112
107
  return unless options[:bff_assets]
113
108
 
114
109
  copy_file(resource_path('bff/gen.template.erb'), "#{target}/resources/bff/gen.template.erb")
115
- copy_file(resource_path('bff/postinstall.sh'), "#{target}/resources/bff/postinstall.sh")
116
- copy_file(resource_path('bff/unpostinstall.sh'), "#{target}/resources/bff/unpostinstall.sh")
117
110
  end
118
111
 
119
112
  def create_deb_assets
@@ -131,12 +124,6 @@ module Omnibus
131
124
  copy_file(resource_path('dmg/icon.png'), "#{target}/resources/dmg/icon.png")
132
125
  end
133
126
 
134
- def create_makeself_assets
135
- return unless options[:makeself_assets]
136
-
137
- copy_file(resource_path('makeself/post_extract.sh.erb'), "#{target}/resources/makeself/post_extract.sh.erb")
138
- end
139
-
140
127
  def create_msi_assets
141
128
  return unless options[:msi_assets]
142
129
 
@@ -105,6 +105,30 @@ module Omnibus
105
105
  )
106
106
  end
107
107
 
108
+ #
109
+ # @!group DSL methods
110
+ # --------------------------------------------------
111
+
112
+ #
113
+ # Retrieve the path at which the project will be installed by the
114
+ # generated package.
115
+ #
116
+ # @return [String]
117
+ #
118
+ def install_dir
119
+ project.install_dir
120
+ end
121
+ expose :install_dir
122
+
123
+ #
124
+ # (see Util#windows_safe_path)
125
+ #
126
+ expose :windows_safe_path
127
+
128
+ #
129
+ # @!endgroup
130
+ # --------------------------------------------------
131
+
108
132
  #
109
133
  # Execute this packager by running the following phases in order:
110
134
  #
@@ -149,7 +173,7 @@ module Omnibus
149
173
  # @return [String]
150
174
  #
151
175
  def staging_dir
152
- @staging_dir ||= Dir.mktmpdir(project.name)
176
+ @staging_dir ||= Dir.mktmpdir(project.package_name)
153
177
  end
154
178
 
155
179
  #
@@ -16,6 +16,15 @@
16
16
 
17
17
  module Omnibus
18
18
  class Packager::BFF < Packager::Base
19
+ # @return [Hash]
20
+ SCRIPT_MAP = {
21
+ # Default Omnibus naming
22
+ preinst: 'Pre-installation Script',
23
+ postinst: 'Post-installation Script',
24
+ prerm: 'Pre_rm Script',
25
+ postrm: 'Unconfiguration Script',
26
+ }.freeze
27
+
19
28
  id :bff
20
29
 
21
30
  setup do
@@ -25,6 +34,9 @@ module Omnibus
25
34
  # /opt/hamlet => /tmp/daj29013/opt/hamlet
26
35
  destination = File.join(staging_dir, project.install_dir)
27
36
  FileSyncer.sync(project.install_dir, destination, exclude: exclusions)
37
+
38
+ # Create the scripts staging directory
39
+ create_directory(scripts_staging_dir)
28
40
  end
29
41
 
30
42
  build do
@@ -37,7 +49,42 @@ module Omnibus
37
49
 
38
50
  # @see Base#package_name
39
51
  def package_name
40
- "#{project.name}.#{bff_version}.#{safe_architecture}.bff"
52
+ "#{safe_base_package_name}.#{bff_version}.#{safe_architecture}.bff"
53
+ end
54
+
55
+ #
56
+ # The path where the package scripts in the install directory.
57
+ #
58
+ # @return [String]
59
+ #
60
+ def scripts_install_dir
61
+ File.expand_path("#{project.install_dir}/embedded/share/installp")
62
+ end
63
+
64
+ #
65
+ # The path where the package scripts will staged.
66
+ #
67
+ # @return [String]
68
+ #
69
+ def scripts_staging_dir
70
+ File.expand_path("#{staging_dir}#{scripts_install_dir}")
71
+ end
72
+
73
+ #
74
+ # Copy all scripts in {Project#package_scripts_path} to the package
75
+ # directory.
76
+ #
77
+ # @return [void]
78
+ #
79
+ def write_scripts
80
+ SCRIPT_MAP.each do |script, _installp_name|
81
+ source_path = File.join(project.package_scripts_path, script.to_s)
82
+
83
+ if File.file?(source_path)
84
+ log.debug(log_key) { "Adding script `#{script}' to `#{scripts_staging_dir}'" }
85
+ copy_file(source_path, scripts_staging_dir)
86
+ end
87
+ end
41
88
  end
42
89
 
43
90
  #
@@ -45,7 +92,50 @@ module Omnibus
45
92
  #
46
93
  # @return [void]
47
94
  #
95
+ # Some details on the various lifecycle scripts:
96
+ #
97
+ # The order of the installp scripts is:
98
+ # - install
99
+ # - pre-install
100
+ # - post-install
101
+ # - config
102
+ # - upgrade
103
+ # - pre-remove (of previous version)
104
+ # - pre-install (previous version of software not present anymore)
105
+ # - post-install
106
+ # - config
107
+ # - remove
108
+ # - unconfig
109
+ # - unpre-install
110
+ #
111
+ # To run the new version of scc, the post-install will do.
112
+ # To run the previous version with an upgrade, use the pre-remove script.
113
+ # To run a source install of scc upon installation of installp package, use the pre-install.
114
+ # Upon upgrade, both the pre-remove and the pre-install scripts will run.
115
+ # As scc has been removed between the runs of these scripts, it will only run once during upgrade.
116
+ #
117
+ # Keywords for scripts:
118
+ #
119
+ # Pre-installation Script: /path/script
120
+ # Unpre-installation Script: /path/script
121
+ # Post-installation Script: /path/script
122
+ # Pre_rm Script: /path/script
123
+ # Configuration Script: /path/script
124
+ # Unconfiguration Script: /path/script
125
+ #
48
126
  def write_gen_template
127
+ # Create a map of scripts that exist and install path
128
+ scripts = SCRIPT_MAP.inject({}) do |hash, (script, installp_key)|
129
+ staging_path = File.join(scripts_staging_dir, script.to_s)
130
+ install_path = File.join(scripts_install_dir, script.to_s)
131
+
132
+ if File.file?(staging_path)
133
+ hash[installp_key] = install_path
134
+ end
135
+
136
+ hash
137
+ end
138
+
49
139
  # Get a list of all files
50
140
  files = FileSyncer.glob("#{staging_dir}/**/*")
51
141
  .map { |path| path.gsub(/^#{staging_dir}/, '') }
@@ -53,16 +143,13 @@ module Omnibus
53
143
  render_template(resource_path('gen.template.erb'),
54
144
  destination: File.join(staging_dir, 'gen.template'),
55
145
  variables: {
56
- name: safe_project_name,
146
+ name: safe_base_package_name,
57
147
  install_dir: project.install_dir,
58
148
  friendly_name: project.friendly_name,
59
149
  version: bff_version,
60
150
  description: project.description,
61
151
  files: files,
62
-
63
- # Add configuration files
64
- configuration_script: resource_path('postinstall.sh'),
65
- unconfiguration_script: resource_path('unpostinstall.sh'),
152
+ scripts: scripts,
66
153
  }
67
154
  )
68
155
  end
@@ -87,21 +174,21 @@ module Omnibus
87
174
  end
88
175
 
89
176
  #
90
- # Return the BFF-ready project name, converting any invalid characters to
177
+ # Return the BFF-ready base package name, converting any invalid characters to
91
178
  # dashes (+-+).
92
179
  #
93
180
  # @return [String]
94
181
  #
95
- def safe_project_name
96
- if project.name =~ /\A[a-z0-9\.\+\-]+\z/
97
- project.name.dup
182
+ def safe_base_package_name
183
+ if project.package_name =~ /\A[a-z0-9\.\+\-]+\z/
184
+ project.package_name.dup
98
185
  else
99
- converted = project.name.downcase.gsub(/[^a-z0-9\.\+\-]+/, '-')
186
+ converted = project.package_name.downcase.gsub(/[^a-z0-9\.\+\-]+/, '-')
100
187
 
101
188
  log.warn(log_key) do
102
189
  "The `name' compontent of BFF package names can only include " \
103
190
  "lowercase alphabetical characters (a-z), numbers (0-9), dots (.), " \
104
- "plus signs (+), and dashes (-). Converting `#{project.name}' to " \
191
+ "plus signs (+), and dashes (-). Converting `#{project.package_name}' to " \
105
192
  "`#{converted}'."
106
193
  end
107
194