omnibus 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.travis.yml +3 -11
  4. data/CHANGELOG.md +50 -0
  5. data/MAINTAINERS.md +26 -0
  6. data/README.md +61 -4
  7. data/appveyor.yml +35 -0
  8. data/docs/Build Cache.md +28 -3
  9. data/docs/Building on RHEL.md +1 -1
  10. data/features/commands/publish.feature +4 -9
  11. data/features/step_definitions/generator_steps.rb +14 -1
  12. data/features/support/env.rb +5 -3
  13. data/lib/omnibus.rb +10 -0
  14. data/lib/omnibus/build_version.rb +34 -25
  15. data/lib/omnibus/build_version_dsl.rb +43 -4
  16. data/lib/omnibus/builder.rb +30 -11
  17. data/lib/omnibus/changelog.rb +52 -0
  18. data/lib/omnibus/changelog_printer.rb +77 -0
  19. data/lib/omnibus/cli.rb +37 -2
  20. data/lib/omnibus/cli/changelog.rb +149 -0
  21. data/lib/omnibus/cli/publish.rb +30 -10
  22. data/lib/omnibus/config.rb +41 -2
  23. data/lib/omnibus/digestable.rb +6 -1
  24. data/lib/omnibus/exceptions.rb +15 -1
  25. data/lib/omnibus/fetcher.rb +78 -34
  26. data/lib/omnibus/fetchers/git_fetcher.rb +84 -42
  27. data/lib/omnibus/fetchers/net_fetcher.rb +64 -13
  28. data/lib/omnibus/fetchers/null_fetcher.rb +8 -1
  29. data/lib/omnibus/fetchers/path_fetcher.rb +24 -1
  30. data/lib/omnibus/file_syncer.rb +52 -1
  31. data/lib/omnibus/generator.rb +22 -21
  32. data/lib/omnibus/generator_files/.kitchen.yml.erb +8 -12
  33. data/lib/omnibus/generator_files/Berksfile.erb +4 -4
  34. data/lib/omnibus/generator_files/Gemfile.erb +3 -3
  35. data/lib/omnibus/generator_files/README.md.erb +17 -0
  36. data/lib/omnibus/generator_files/omnibus.rb.erb +6 -0
  37. data/lib/omnibus/git_repository.rb +43 -0
  38. data/lib/omnibus/health_check.rb +5 -1
  39. data/lib/omnibus/manifest.rb +134 -0
  40. data/lib/omnibus/manifest_diff.rb +88 -0
  41. data/lib/omnibus/manifest_entry.rb +43 -0
  42. data/lib/omnibus/metadata.rb +19 -1
  43. data/lib/omnibus/package.rb +9 -0
  44. data/lib/omnibus/packagers/base.rb +1 -1
  45. data/lib/omnibus/packagers/bff.rb +5 -5
  46. data/lib/omnibus/packagers/deb.rb +11 -4
  47. data/lib/omnibus/packagers/msi.rb +243 -2
  48. data/lib/omnibus/packagers/rpm.rb +68 -14
  49. data/lib/omnibus/packagers/solaris.rb +17 -23
  50. data/lib/omnibus/project.rb +129 -16
  51. data/lib/omnibus/publisher.rb +62 -49
  52. data/lib/omnibus/publishers/artifactory_publisher.rb +96 -5
  53. data/lib/omnibus/publishers/s3_publisher.rb +20 -25
  54. data/lib/omnibus/s3_cache.rb +13 -34
  55. data/lib/omnibus/s3_helpers.rb +119 -0
  56. data/lib/omnibus/semantic_version.rb +57 -0
  57. data/lib/omnibus/software.rb +87 -28
  58. data/lib/omnibus/sugarable.rb +18 -0
  59. data/lib/omnibus/templating.rb +8 -1
  60. data/lib/omnibus/version.rb +1 -1
  61. data/omnibus.gemspec +10 -7
  62. data/resources/bff/gen.template.erb +1 -1
  63. data/resources/msi/bundle.wxs.erb +17 -0
  64. data/resources/msi/localization-en-us.wxl.erb +1 -1
  65. data/resources/rpm/spec.erb +1 -1
  66. data/spec/functional/builder_spec.rb +15 -7
  67. data/spec/functional/fetchers/git_fetcher_spec.rb +44 -12
  68. data/spec/functional/fetchers/net_fetcher_spec.rb +171 -20
  69. data/spec/functional/fetchers/path_fetcher_spec.rb +16 -1
  70. data/spec/functional/file_syncer_spec.rb +58 -5
  71. data/spec/functional/templating_spec.rb +17 -6
  72. data/spec/spec_helper.rb +17 -0
  73. data/spec/support/file_helpers.rb +12 -2
  74. data/spec/support/git_helpers.rb +23 -18
  75. data/spec/support/matchers.rb +22 -0
  76. data/spec/support/output_helpers.rb +29 -0
  77. data/spec/unit/build_version_dsl_spec.rb +31 -4
  78. data/spec/unit/build_version_spec.rb +11 -4
  79. data/spec/unit/builder_spec.rb +33 -0
  80. data/spec/unit/changelog_spec.rb +55 -0
  81. data/spec/unit/cleanroom_spec.rb +1 -1
  82. data/spec/unit/compressors/dmg_spec.rb +3 -3
  83. data/spec/unit/compressors/tgz_spec.rb +3 -3
  84. data/spec/unit/config_spec.rb +3 -1
  85. data/spec/unit/fetcher_spec.rb +35 -0
  86. data/spec/unit/fetchers/git_fetcher_spec.rb +28 -14
  87. data/spec/unit/fetchers/net_fetcher_spec.rb +178 -24
  88. data/spec/unit/fetchers/path_fetcher_spec.rb +8 -7
  89. data/spec/unit/generator_spec.rb +22 -21
  90. data/spec/unit/git_repository_spec.rb +60 -0
  91. data/spec/unit/manifest_diff_spec.rb +75 -0
  92. data/spec/unit/manifest_spec.rb +116 -0
  93. data/spec/unit/metadata_spec.rb +11 -0
  94. data/spec/unit/omnibus_spec.rb +9 -6
  95. data/spec/unit/package_spec.rb +1 -1
  96. data/spec/unit/packagers/base_spec.rb +8 -18
  97. data/spec/unit/packagers/bff_spec.rb +9 -5
  98. data/spec/unit/packagers/deb_spec.rb +44 -12
  99. data/spec/unit/packagers/makeself_spec.rb +4 -4
  100. data/spec/unit/packagers/msi_spec.rb +122 -6
  101. data/spec/unit/packagers/pkg_spec.rb +3 -3
  102. data/spec/unit/packagers/rpm_spec.rb +37 -12
  103. data/spec/unit/project_spec.rb +18 -1
  104. data/spec/unit/publisher_spec.rb +65 -0
  105. data/spec/unit/publishers/artifactory_publisher_spec.rb +26 -20
  106. data/spec/unit/publishers/s3_publisher_spec.rb +14 -30
  107. data/spec/unit/s3_cacher_spec.rb +1 -1
  108. data/spec/unit/s3_helpers_spec.rb +32 -0
  109. data/spec/unit/semantic_version_spec.rb +55 -0
  110. data/spec/unit/software_spec.rb +112 -4
  111. metadata +86 -16
@@ -10,11 +10,20 @@ module Omnibus
10
10
  { path: source_path }
11
11
  end
12
12
 
13
+ let(:manifest_entry) do
14
+ double(Omnibus::ManifestEntry,
15
+ name: 'pathelogical',
16
+ locked_version: nil,
17
+ described_version: nil,
18
+ locked_source: source)
19
+ end
20
+
13
21
  before do
14
22
  create_directory(source_path)
15
23
  end
16
24
 
17
- subject { described_class.new(software) }
25
+ subject { described_class.new(manifest_entry, project_dir, build_dir) }
26
+
18
27
 
19
28
  describe '#fetch_required?' do
20
29
  context 'when the directories have different files' do
@@ -93,5 +102,11 @@ module Omnibus
93
102
  expect(subject.version_for_cache).to eq("path:#{source_path}|shasum:#{sha}")
94
103
  end
95
104
  end
105
+
106
+ describe '#resolve_version' do
107
+ it 'just returns the version' do
108
+ expect(NetFetcher.resolve_version("1.2.3", source)).to eq("1.2.3")
109
+ end
110
+ end
96
111
  end
97
112
  end
@@ -75,6 +75,30 @@ module Omnibus
75
75
  end
76
76
  end
77
77
 
78
+ context 'when destination file exists' do
79
+
80
+ let(:source) {
81
+ s = File.join(tmp_path, 'source')
82
+ FileUtils.mkdir_p(s)
83
+ p = create_file(s, 'read-only-file') { 'new' }
84
+ FileUtils.chmod(0400, p)
85
+ s
86
+ }
87
+
88
+ let(:destination) {
89
+ dest = File.join(tmp_path, 'destination')
90
+ FileUtils.mkdir_p(dest)
91
+ create_file(dest, 'read-only-file') { 'old' }
92
+ FileUtils.chmod(0400, File.join(dest, 'read-only-file'))
93
+ dest
94
+ }
95
+
96
+ it 'copies over a read-only file' do
97
+ described_class.sync(source, destination)
98
+ expect("#{destination}/read-only-file").to have_content "new"
99
+ end
100
+ end
101
+
78
102
  context 'when the directory exists' do
79
103
  before { FileUtils.mkdir_p(destination) }
80
104
 
@@ -101,7 +125,36 @@ module Omnibus
101
125
  end
102
126
  end
103
127
 
104
- context 'with deeply nested paths and symlinks' do
128
+ context 'when target files are hard links' do
129
+ let(:source) do
130
+ source = File.join(tmp_path, 'source')
131
+ FileUtils.mkdir_p(source)
132
+
133
+ create_directory(source, 'bin')
134
+ create_file(source, 'bin', 'git')
135
+ FileUtils.ln("#{source}/bin/git", "#{source}/bin/git-tag")
136
+ FileUtils.ln("#{source}/bin/git", "#{source}/bin/git-write-tree")
137
+
138
+ source
139
+ end
140
+
141
+ it 'copies the first instance and links to that instance thereafter' do
142
+ FileUtils.mkdir_p("#{destination}/bin")
143
+
144
+ described_class.sync(source, destination)
145
+
146
+ expect("#{destination}/bin/git").to be_a_file
147
+ if windows?
148
+ expect("#{destination}/bin/git-tag").to be_a_file
149
+ expect("#{destination}/bin/git-write-tree").to be_a_file
150
+ else
151
+ expect("#{destination}/bin/git-tag").to be_a_hardlink
152
+ expect("#{destination}/bin/git-write-tree").to be_a_hardlink
153
+ end
154
+ end
155
+ end
156
+
157
+ context 'with deeply nested paths and symlinks', :not_supported_on_windows do
105
158
  let(:source) do
106
159
  source = File.join(tmp_path, 'source')
107
160
  FileUtils.mkdir_p(source)
@@ -125,10 +178,10 @@ module Omnibus
125
178
  create_directory(source, 'empty_directory')
126
179
 
127
180
  create_directory(source, 'links')
128
- create_file(source, 'links', 'home.html')
129
- FileUtils.ln_s("./home.html", "#{source}/links/index.html")
130
- FileUtils.ln_s("./home.html", "#{source}/links/default.html")
131
- FileUtils.ln_s("../source/bin/apt", "#{source}/links/apt")
181
+ create_file(source, 'links', 'home.html')
182
+ FileUtils.ln_s('./home.html', "#{source}/links/index.html")
183
+ FileUtils.ln_s('./home.html', "#{source}/links/default.html")
184
+ FileUtils.ln_s('../source/bin/apt', "#{source}/links/apt")
132
185
 
133
186
  FileUtils.ln_s('/foo/bar', "#{source}/root")
134
187
 
@@ -9,8 +9,8 @@ module Omnibus
9
9
  subject { RandomClass.new }
10
10
 
11
11
  describe '#render_template' do
12
- let(:source) { "#{tmp_path}/source.erb" }
13
- let(:destination) { "#{tmp_path}/final" }
12
+ let(:source) { File.join(tmp_path, 'source.erb') }
13
+ let(:destination) { File.join(tmp_path, 'final') }
14
14
  let(:mode) { 0644 }
15
15
  let(:variables) { { name: 'Name' } }
16
16
  let(:contents) do
@@ -40,19 +40,18 @@ module Omnibus
40
40
 
41
41
  it 'renders adjacent, without the erb extension' do
42
42
  subject.render_template(source, options)
43
- expect("#{tmp_path}/source").to be_a_file
43
+ expect(File.join(tmp_path, 'source')).to be_a_file
44
44
  end
45
45
  end
46
46
 
47
47
  context 'when a destination is given' do
48
-
49
48
  it 'renders at the destination' do
50
49
  subject.render_template(source, options)
51
50
  expect(destination).to be_a_file
52
51
  end
53
52
  end
54
53
 
55
- context 'when a mode is given' do
54
+ context 'when a mode is given', :not_supported_on_windows do
56
55
  let(:mode) { 0755 }
57
56
 
58
57
  it 'renders the object with the mode' do
@@ -65,7 +64,19 @@ module Omnibus
65
64
  let(:contents) { "<%= not_a_real_variable %>" }
66
65
 
67
66
  it 'raise an exception' do
68
- expect { subject.render_template(source, options) }.to raise_error
67
+ expect do
68
+ subject.render_template(source, options)
69
+ end.to raise_error(NameError)
70
+ end
71
+ end
72
+
73
+ context 'when no variables are present' do
74
+ let(:content) { "static content" }
75
+ let(:variables) { {} }
76
+
77
+ it 'renders the template' do
78
+ subject.render_template(source, options)
79
+ expect(destination).to be_a_file
69
80
  end
70
81
  end
71
82
  end
@@ -1,5 +1,6 @@
1
1
  require 'rspec'
2
2
  require 'rspec/its'
3
+ require 'webmock/rspec'
3
4
 
4
5
  require 'cleanroom/rspec'
5
6
 
@@ -33,6 +34,9 @@ RSpec.configure do |config|
33
34
  require_relative 'support/ohai_helpers'
34
35
  config.include(Omnibus::RSpec::OhaiHelpers)
35
36
 
37
+ require_relative 'support/output_helpers'
38
+ config.include(Omnibus::RSpec::OutputHelpers)
39
+
36
40
  require_relative 'support/path_helpers'
37
41
  config.include(Omnibus::RSpec::PathHelpers)
38
42
 
@@ -44,6 +48,15 @@ RSpec.configure do |config|
44
48
 
45
49
  config.filter_run_excluding(windows_only: true) unless windows?
46
50
  config.filter_run_excluding(mac_only: true) unless mac?
51
+ config.filter_run_excluding(not_supported_on_windows: true) if windows?
52
+
53
+ if config.files_to_run.one?
54
+ # Use the documentation formatter for detailed output,
55
+ # unless a formatter has already been configured
56
+ # (e.g. via a command-line flag).
57
+ config.default_formatter = 'doc'
58
+ config.color = true
59
+ end
47
60
 
48
61
  config.before(:each) do
49
62
  # Suppress logging
@@ -51,6 +64,7 @@ RSpec.configure do |config|
51
64
 
52
65
  # Reset config
53
66
  Omnibus.reset!
67
+ Omnibus::Config.append_timestamp(false)
54
68
 
55
69
  # Clear the tmp_path on each run
56
70
  FileUtils.rm_rf(tmp_path)
@@ -58,6 +72,9 @@ RSpec.configure do |config|
58
72
 
59
73
  # Don't run Ohai - tests can still override this
60
74
  stub_ohai(platform: 'ubuntu', version: '12.04')
75
+
76
+ # Default to real HTTP requests
77
+ WebMock.allow_net_connect!
61
78
  end
62
79
 
63
80
  config.after(:each) do
@@ -28,6 +28,7 @@ module Omnibus
28
28
  path = File.join(*paths)
29
29
 
30
30
  FileUtils.mkdir_p(File.dirname(path))
31
+ FileUtils.rm(path) if File.exist?(path)
31
32
 
32
33
  if block
33
34
  File.open(path, 'wb') { |f| f.write(block.call) }
@@ -38,8 +39,17 @@ module Omnibus
38
39
  path
39
40
  end
40
41
 
41
- def create_link(a, b)
42
- FileUtils.ln_s(a, b)
42
+ def create_link(src, dest)
43
+ # Windows has no symlinks. Documentation seems to suggest that
44
+ # ln will create hard-links - so attempt to elicit the behavior
45
+ # we want using hard-links. If your test happens to fail even
46
+ # with this, consider what semantics you actually wish to have
47
+ # on windows and refactor your test or code.
48
+ if windows?
49
+ FileUtils.ln(src, dest) unless File.exist?(dest)
50
+ else
51
+ FileUtils.ln_s(src, dest) unless File.exist?(dest)
52
+ end
43
53
  end
44
54
  end
45
55
  end
@@ -9,28 +9,16 @@ module Omnibus
9
9
  "file://#{fake_git_remote("git://github.com/omnibus/#{repo}.git", options)}/.git"
10
10
  end
11
11
 
12
- def remote_git_repo(name, options = {})
13
- path = File.join(remotes, name)
14
- remote_url = "file://#{path}"
15
-
16
- # Create a bogus software
17
- FileUtils.mkdir_p(path)
18
-
12
+ def local_git_repo(name, options={})
13
+ path = git_scratch
19
14
  Dir.chdir(path) do
20
- git %|init --bare|
21
- git %|config core.sharedrepository 1|
22
- git %|config receive.denyNonFastforwards true|
23
- git %|config receive.denyCurrentBranch ignore|
24
- end
25
-
26
- Dir.chdir(git_scratch) do
27
15
  # Create a bogus configure file
28
16
  File.open('configure', 'w') { |f| f.write('echo "Done!"') }
29
17
 
30
18
  git %|init .|
31
19
  git %|add .|
32
20
  git %|commit -am "Initial commit for #{name}..."|
33
- git %|remote add origin "#{remote_url}"|
21
+ git %|remote add origin "#{options[:remote]}"| if options[:remote]
34
22
  git %|push origin master|
35
23
 
36
24
  options[:annotated_tags].each do |tag|
@@ -38,7 +26,7 @@ module Omnibus
38
26
  git %|add tag|
39
27
  git %|commit -am "Create tag #{tag}"|
40
28
  git %|tag "#{tag}" -m "#{tag}"|
41
- git %|push origin "#{tag}"|
29
+ git %|push origin "#{tag}"| if options[:remote]
42
30
  end if options[:annotated_tags]
43
31
 
44
32
  options[:tags].each do |tag|
@@ -46,7 +34,7 @@ module Omnibus
46
34
  git %|add tag|
47
35
  git %|commit -am "Create tag #{tag}"|
48
36
  git %|tag "#{tag}"|
49
- git %|push origin "#{tag}"|
37
+ git %|push origin "#{tag}"| if options[:remote]
50
38
  end if options[:tags]
51
39
 
52
40
  options[:branches].each do |branch|
@@ -54,11 +42,28 @@ module Omnibus
54
42
  File.open('branch', 'w') { |f| f.write(branch) }
55
43
  git %|add branch|
56
44
  git %|commit -am "Create branch #{branch}"|
57
- git %|push origin "#{branch}"|
45
+ git %|push origin "#{branch}"| if options[:remote]
58
46
  git %|checkout master|
59
47
  end if options[:branches]
60
48
  end
49
+ path
50
+ end
51
+
52
+ def remote_git_repo(name, options = {})
53
+ path = File.join(remotes, name)
54
+ remote_url = "file://#{path}"
55
+ options[:remote] = remote_url
56
+ # Create a bogus software
57
+ FileUtils.mkdir_p(path)
58
+
59
+ Dir.chdir(path) do
60
+ git %|init --bare|
61
+ git %|config core.sharedrepository 1|
62
+ git %|config receive.denyNonFastforwards true|
63
+ git %|config receive.denyCurrentBranch ignore|
64
+ end
61
65
 
66
+ local_git_repo(name, options)
62
67
  path
63
68
  end
64
69
 
@@ -14,6 +14,21 @@ RSpec::Matchers.define :be_a_file do
14
14
  end
15
15
  end
16
16
 
17
+ # expect('/path/to/file').to have_content
18
+ RSpec::Matchers.define :have_content do |content|
19
+ match do |actual|
20
+ IO.read(actual) == content
21
+ end
22
+ end
23
+
24
+ # expect('/path/to/file').to have_permissions
25
+ RSpec::Matchers.define :have_permissions do |perm|
26
+ match do |actual|
27
+ m = sprintf('%o', File.stat(actual).mode)
28
+ m == perm
29
+ end
30
+ end
31
+
17
32
  # expect('/path/to/directory').to be_a_symlink
18
33
  RSpec::Matchers.define :be_a_symlink do
19
34
  match do |actual|
@@ -34,3 +49,10 @@ RSpec::Matchers.define :be_an_executable do
34
49
  File.executable?(actual)
35
50
  end
36
51
  end
52
+
53
+ # expect('/path/to/file').to be_a_hardlink
54
+ RSpec::Matchers.define :be_a_hardlink do |path|
55
+ match do |actual|
56
+ File.stat(actual).nlink > 2
57
+ end
58
+ end
@@ -0,0 +1,29 @@
1
+ module Omnibus
2
+ module RSpec
3
+ module OutputHelpers
4
+ #
5
+ # Capture stdout within this block.
6
+ #
7
+ def capture_stdout(&block)
8
+ old = $stdout
9
+ $stdout = fake = StringIO.new
10
+ yield
11
+ fake.string
12
+ ensure
13
+ $stdout = old
14
+ end
15
+
16
+ #
17
+ # Capture stderr within this block.
18
+ #
19
+ def capture_stderr(&block)
20
+ old = $stderr
21
+ $stderr = fake = StringIO.new
22
+ yield
23
+ fake.string
24
+ ensure
25
+ $stderr = old
26
+ end
27
+ end
28
+ end
29
+ end
@@ -7,13 +7,14 @@ module Omnibus
7
7
 
8
8
  let(:version_string) { "1.0.0" }
9
9
  let(:description) { nil }
10
+ let(:today_string) { Time.now.utc.strftime(Omnibus::BuildVersion::TIMESTAMP_FORMAT) }
10
11
 
11
- let(:zoo_version) { double("BuildVersion", semver: "5.5.5", custom: "7.7.7") }
12
+ let(:zoo_version) { double("BuildVersion", semver: "5.5.5", custom: "7.7.7", build_start_time: today_string) }
12
13
  let(:zoo_software) { double("software", name: 'zoo', project_dir: '/etc/zoo', version: "6.6.6") }
13
14
 
14
15
  describe "when given nil" do
15
16
  it "fails" do
16
- expect { subject.build_version }.to raise_error
17
+ expect { subject.build_version }.to raise_error(RuntimeError)
17
18
  end
18
19
  end
19
20
 
@@ -23,6 +24,32 @@ module Omnibus
23
24
  end
24
25
  end
25
26
 
27
+ describe "when Config.append_timestamp is true" do
28
+ let(:description) do
29
+ proc do
30
+ source(:git, from_dependency: 'zoo')
31
+ end
32
+ end
33
+
34
+ before { Config.append_timestamp(true) }
35
+
36
+ it "appends a timestamp to a static (String) version" do
37
+ expect(subject_with_version.build_version).to eq("1.0.0+#{today_string}")
38
+ end
39
+
40
+ it "doesn't append timestamp to something that already looks like it has a timestamp" do
41
+ semver = "1.0.0+#{today_string}.git.222.694b062"
42
+ expect(described_class.new(semver).build_version).to eq("1.0.0+#{today_string}.git.222.694b062")
43
+ end
44
+
45
+ it "appends a timestamp to a DSL-built version" do
46
+ allow(BuildVersion).to receive(:new).and_return(BuildVersion.new)
47
+ allow(BuildVersion).to receive(:new).with("/etc/zoo").and_return(zoo_version)
48
+ subject_with_description.resolve(zoo_software)
49
+ expect(subject_with_description.build_version).to eq("5.5.5+#{today_string}")
50
+ end
51
+ end
52
+
26
53
  describe "when given a :git source" do
27
54
  describe "when given a software as source" do
28
55
  let(:description) do
@@ -101,7 +128,7 @@ module Omnibus
101
128
  end
102
129
 
103
130
  it "fails" do
104
- expect { subject_with_description.build_version }.to raise_error
131
+ expect { subject_with_description.build_version }.to raise_error(RuntimeError)
105
132
  end
106
133
  end
107
134
  end
@@ -114,7 +141,7 @@ module Omnibus
114
141
  end
115
142
 
116
143
  it "fails" do
117
- expect { subject_with_description.build_version }.to raise_error
144
+ expect { subject_with_description.build_version }.to raise_error(RuntimeError)
118
145
  end
119
146
  end
120
147
  end