realityforge-buildr 1.5.9

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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE +176 -0
  4. data/NOTICE +26 -0
  5. data/README.md +3 -0
  6. data/Rakefile +50 -0
  7. data/addon/buildr/checkstyle-report.xsl +104 -0
  8. data/addon/buildr/checkstyle.rb +254 -0
  9. data/addon/buildr/git_auto_version.rb +36 -0
  10. data/addon/buildr/gpg.rb +90 -0
  11. data/addon/buildr/gwt.rb +413 -0
  12. data/addon/buildr/jacoco.rb +161 -0
  13. data/addon/buildr/pmd.rb +185 -0
  14. data/addon/buildr/single_intermediate_layout.rb +71 -0
  15. data/addon/buildr/spotbugs.rb +265 -0
  16. data/addon/buildr/top_level_generate_dir.rb +37 -0
  17. data/addon/buildr/wsgen.rb +192 -0
  18. data/bin/buildr +20 -0
  19. data/buildr.gemspec +61 -0
  20. data/lib/buildr.rb +86 -0
  21. data/lib/buildr/core/application.rb +705 -0
  22. data/lib/buildr/core/assets.rb +96 -0
  23. data/lib/buildr/core/build.rb +587 -0
  24. data/lib/buildr/core/common.rb +167 -0
  25. data/lib/buildr/core/compile.rb +599 -0
  26. data/lib/buildr/core/console.rb +124 -0
  27. data/lib/buildr/core/doc.rb +275 -0
  28. data/lib/buildr/core/environment.rb +128 -0
  29. data/lib/buildr/core/filter.rb +405 -0
  30. data/lib/buildr/core/help.rb +114 -0
  31. data/lib/buildr/core/progressbar.rb +161 -0
  32. data/lib/buildr/core/project.rb +994 -0
  33. data/lib/buildr/core/test.rb +776 -0
  34. data/lib/buildr/core/transports.rb +456 -0
  35. data/lib/buildr/core/util.rb +77 -0
  36. data/lib/buildr/ide/idea.rb +1664 -0
  37. data/lib/buildr/java/commands.rb +230 -0
  38. data/lib/buildr/java/compiler.rb +85 -0
  39. data/lib/buildr/java/custom_pom.rb +300 -0
  40. data/lib/buildr/java/doc.rb +62 -0
  41. data/lib/buildr/java/packaging.rb +393 -0
  42. data/lib/buildr/java/pom.rb +191 -0
  43. data/lib/buildr/java/test_result.rb +54 -0
  44. data/lib/buildr/java/tests.rb +111 -0
  45. data/lib/buildr/packaging/archive.rb +586 -0
  46. data/lib/buildr/packaging/artifact.rb +1113 -0
  47. data/lib/buildr/packaging/artifact_namespace.rb +1010 -0
  48. data/lib/buildr/packaging/artifact_search.rb +138 -0
  49. data/lib/buildr/packaging/package.rb +237 -0
  50. data/lib/buildr/packaging/version_requirement.rb +189 -0
  51. data/lib/buildr/packaging/zip.rb +189 -0
  52. data/lib/buildr/packaging/ziptask.rb +387 -0
  53. data/lib/buildr/version.rb +18 -0
  54. data/rakelib/release.rake +99 -0
  55. data/spec/addon/checkstyle_spec.rb +58 -0
  56. data/spec/core/application_spec.rb +576 -0
  57. data/spec/core/build_spec.rb +922 -0
  58. data/spec/core/common_spec.rb +670 -0
  59. data/spec/core/compile_spec.rb +656 -0
  60. data/spec/core/console_spec.rb +65 -0
  61. data/spec/core/doc_spec.rb +194 -0
  62. data/spec/core/extension_spec.rb +200 -0
  63. data/spec/core/project_spec.rb +736 -0
  64. data/spec/core/test_spec.rb +1131 -0
  65. data/spec/core/transport_spec.rb +452 -0
  66. data/spec/core/util_spec.rb +154 -0
  67. data/spec/ide/idea_spec.rb +1952 -0
  68. data/spec/java/commands_spec.rb +79 -0
  69. data/spec/java/compiler_spec.rb +274 -0
  70. data/spec/java/custom_pom_spec.rb +165 -0
  71. data/spec/java/doc_spec.rb +55 -0
  72. data/spec/java/packaging_spec.rb +786 -0
  73. data/spec/java/pom_spec.rb +162 -0
  74. data/spec/java/test_coverage_helper.rb +257 -0
  75. data/spec/java/tests_spec.rb +224 -0
  76. data/spec/packaging/archive_spec.rb +686 -0
  77. data/spec/packaging/artifact_namespace_spec.rb +757 -0
  78. data/spec/packaging/artifact_spec.rb +1351 -0
  79. data/spec/packaging/packaging_helper.rb +63 -0
  80. data/spec/packaging/packaging_spec.rb +690 -0
  81. data/spec/sandbox.rb +166 -0
  82. data/spec/spec_helpers.rb +420 -0
  83. data/spec/version_requirement_spec.rb +145 -0
  84. data/spec/xpath_matchers.rb +123 -0
  85. metadata +295 -0
@@ -0,0 +1,456 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one or more
2
+ # contributor license agreements. See the NOTICE file distributed with this
3
+ # work for additional information regarding copyright ownership. The ASF
4
+ # licenses this file to you under the Apache License, Version 2.0 (the
5
+ # "License"); you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations under
14
+ # the License.
15
+
16
+ require 'net/http'
17
+ autoload :CGI, 'cgi'
18
+ require 'digest/md5'
19
+ require 'digest/sha1'
20
+ autoload :ProgressBar, 'buildr/core/progressbar'
21
+
22
+ # Not quite open-uri, but similar. Provides read and write methods for the resource represented by the URI.
23
+ # Currently supports reads for URI::HTTP. Also provides convenience methods for
24
+ # downloads and uploads.
25
+ module URI
26
+
27
+ # Raised when trying to read/download a resource that doesn't exist.
28
+ class NotFoundError < RuntimeError
29
+ end
30
+
31
+ # How many bytes to read/write at once. Do not change without checking BUILDR-214 first.
32
+ RW_CHUNK_SIZE = 128 * 1024 #:nodoc:
33
+
34
+ class << self
35
+ # :call-seq:
36
+ # read(uri, options?) => content
37
+ # read(uri, options?) { |chunk| ... }
38
+ #
39
+ # Reads from the resource behind this URI. The first form returns the content of the resource,
40
+ # the second form yields to the block with each chunk of content (usually more than one).
41
+ #
42
+ # For example:
43
+ # File.open 'image.jpg', 'w' do |file|
44
+ # URI.read('http://example.com/image.jpg') { |chunk| file.write chunk }
45
+ # end
46
+ # Shorter version:
47
+ # File.open('image.jpg', 'w') { |file| file.write URI.read('http://example.com/image.jpg') }
48
+ #
49
+ # Supported options:
50
+ # * :modified -- Only download if file modified since this timestamp. Returns nil if not modified.
51
+ # * :progress -- Show the progress bar while reading.
52
+ def read(uri, options = nil, &block)
53
+ uri = URI.parse(uri.to_s) unless URI === uri
54
+ uri.read options, &block
55
+ end
56
+
57
+ # :call-seq:
58
+ # download(uri, target, options?)
59
+ #
60
+ # Downloads the resource to the target.
61
+ #
62
+ # The target may be a file name (string or task), in which case the file is created from the resource.
63
+ # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
64
+ #
65
+ # Use the progress bar when running in verbose mode.
66
+ def download(uri, target, options = nil)
67
+ uri = URI.parse(uri.to_s) unless URI === uri
68
+ uri.download target, options
69
+ end
70
+
71
+ # :call-seq:
72
+ # write(uri, content, options?)
73
+ # write(uri, options?) { |bytes| .. }
74
+ #
75
+ # Writes to the resource behind the URI. The first form writes the content from a string or an object
76
+ # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
77
+ # block. Each yield should return up to the specified number of bytes, the last yield returns nil.
78
+ #
79
+ # For example:
80
+ # File.open 'killer-app.jar', 'rb' do |file|
81
+ # write('https://localhost/jars/killer-app.jar') { |chunk| file.read(chunk) }
82
+ # end
83
+ # Or:
84
+ # write 'https://localhost/jars/killer-app.jar', File.read('killer-app.jar')
85
+ #
86
+ # Supported options:
87
+ # * :progress -- Show the progress bar while reading.
88
+ def write(uri, *args, &block)
89
+ uri = URI.parse(uri.to_s) unless URI === uri
90
+ uri.write *args, &block
91
+ end
92
+
93
+ # :call-seq:
94
+ # upload(uri, source, options?)
95
+ #
96
+ # Uploads from source to the resource.
97
+ #
98
+ # The source may be a file name (string or task), in which case the file is uploaded to the resource.
99
+ # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
100
+ #
101
+ # Use the progress bar when running in verbose mode.
102
+ def upload(uri, source, options = nil)
103
+ uri = URI.parse(uri.to_s) unless URI === uri
104
+ uri.upload source, options
105
+ end
106
+
107
+ end
108
+
109
+ class Generic
110
+
111
+ # :call-seq:
112
+ # read(options?) => content
113
+ # read(options?) { |chunk| ... }
114
+ #
115
+ # Reads from the resource behind this URI. The first form returns the content of the resource,
116
+ # the second form yields to the block with each chunk of content (usually more than one).
117
+ #
118
+ # For options, see URI::read.
119
+ def read(options = nil, &block)
120
+ fail 'This protocol doesn\'t support reading (yet, how about helping by implementing it?)'
121
+ end
122
+
123
+ # :call-seq:
124
+ # download(target, options?)
125
+ #
126
+ # Downloads the resource to the target.
127
+ #
128
+ # The target may be a file name (string or task), in which case the file is created from the resource.
129
+ # The target may also be any object that responds to +write+, e.g. File, StringIO, Pipe.
130
+ #
131
+ # Use the progress bar when running in verbose mode.
132
+ def download(target, options = nil)
133
+ case target
134
+ when Rake::Task
135
+ download target.name, options
136
+ when String
137
+ # If download breaks we end up with a partial file which is
138
+ # worse than not having a file at all, so download to temporary
139
+ # file and then move over.
140
+ modified = ::File.stat(target).mtime if ::File.exist?(target)
141
+ temp = Tempfile.new(::File.basename(target))
142
+ temp.binmode
143
+ written = false
144
+ read({ :progress => verbose }.merge(options || {}).merge(:modified => modified)) { |chunk| written = true; temp.write chunk }
145
+ temp.close
146
+ mkpath ::File.dirname(target)
147
+ # Only attempt to override file if it was actually written to, i.e. "HTTP Not Modified" was not returned.
148
+ mv temp.path, target if written
149
+ when ::File
150
+ read({ :progress => verbose }.merge(options || {}).merge(:modified => target.mtime)) { |chunk| target.write chunk }
151
+ target.flush
152
+ else
153
+ raise ArgumentError, 'Expecting a target that is either a file name (string, task) or object that responds to write (file, pipe).' unless target.respond_to?(:write)
154
+ read({:progress=>verbose}.merge(options || {})) { |chunk| target.write chunk }
155
+ target.flush
156
+ end
157
+ end
158
+
159
+ # :call-seq:
160
+ # write(content, options?)
161
+ # write(options?) { |bytes| .. }
162
+ #
163
+ # Writes to the resource behind the URI. The first form writes the content from a string or an object
164
+ # that responds to +read+ and optionally +size+. The second form writes the content by yielding to the
165
+ # block. Each yield should return up to the specified number of bytes, the last yield returns nil.
166
+ #
167
+ # For options, see URI::write.
168
+ def write(*args, &block)
169
+ options = args.pop if Hash === args.last
170
+ options ||= {}
171
+ if String === args.first
172
+ ios = StringIO.new(args.first, 'r')
173
+ write(options.merge(:size=>args.first.size)) { |bytes| ios.read(bytes) }
174
+ elsif args.first.respond_to?(:read)
175
+ size = args.first.size rescue nil
176
+ write({:size=>size}.merge(options)) { |bytes| args.first.read(bytes) }
177
+ elsif args.empty? && block
178
+ write_internal options, &block
179
+ else
180
+ raise ArgumentError, 'Either give me the content, or pass me a block, otherwise what would I upload?'
181
+ end
182
+ end
183
+
184
+ # :call-seq:
185
+ # upload(source, options?)
186
+ #
187
+ # Uploads from source to the resource.
188
+ #
189
+ # The source may be a file name (string or task), in which case the file is uploaded to the resource.
190
+ # If the source is a directory, uploads all files inside the directory (including nested directories).
191
+ # The source may also be any object that responds to +read+ (and optionally +size+), e.g. File, StringIO, Pipe.
192
+ #
193
+ # Use the progress bar when running in verbose mode.
194
+ def upload(source, options = nil)
195
+ source = source.name if Rake::Task === source
196
+ options ||= {}
197
+ if String === source
198
+ raise NotFoundError, 'No source file/directory to upload.' unless ::File.exist?(source)
199
+ if ::File.directory?(source)
200
+ Dir.glob("#{source}/**/*").reject { |file| ::File.directory?(file) }.each do |file|
201
+ uri = self + (::File.join(self.path, file.sub(source, '')))
202
+ uri.upload file, { :digests => [] }.merge(options)
203
+ end
204
+ else
205
+ ::File.open(source, 'rb') { |input| upload input, options }
206
+ end
207
+ elsif source.respond_to?(:read)
208
+ digests = (options[:digests] || [:md5, :sha1]).
209
+ inject({}) { |hash, name| hash[name] = name.to_s == 'sha512' ? Digest::SHA2.new(512) : Digest.const_get(name.to_s.upcase).new ; hash}
210
+ size = source.stat.size rescue nil
211
+ write (options).merge(:progress=>verbose && size, :size=>size) do |bytes|
212
+ source.read(bytes).tap do |chunk|
213
+ digests.values.each { |digest| digest << chunk } if chunk
214
+ end
215
+ end
216
+ digests.each do |key, digest|
217
+ self.merge("#{self.path}.#{key}").write digest.hexdigest,
218
+ (options).merge(:progress=>false)
219
+ end
220
+ else
221
+ raise ArgumentError, 'Expecting source to be a file name (string, task) or any object that responds to read (file, pipe).'
222
+ end
223
+ end
224
+
225
+ protected
226
+
227
+ # :call-seq:
228
+ # with_progress_bar(show, file_name, size) { |progress| ... }
229
+ #
230
+ # Displays a progress bar while executing the block. The first argument must be true for the
231
+ # progress bar to show (TTY output also required), as a convenient for selectively using the
232
+ # progress bar from a single block.
233
+ #
234
+ # The second argument provides a filename to display, the third its size in bytes.
235
+ #
236
+ # The block is yielded with a progress object that implements a single method.
237
+ # Call << for each block of bytes down/uploaded.
238
+ def with_progress_bar(show, file_name, size, &block) #:nodoc:
239
+ options = { :total=>size || 0, :title=>file_name }
240
+ options[:hidden] = true unless show
241
+ ProgressBar.start options, &block
242
+ end
243
+
244
+ # :call-seq:
245
+ # proxy_uri => URI?
246
+ #
247
+ # Returns the proxy server to use. Obtains the proxy from the relevant environment variable (e.g. HTTP_PROXY).
248
+ # Supports exclusions based on host name and port number from environment variable NO_PROXY.
249
+ def proxy_uri
250
+ proxy = ENV["#{scheme.upcase}_PROXY"]
251
+ proxy = URI.parse(proxy) if String === proxy
252
+ excludes = ENV['NO_PROXY'].to_s.split(/\s*,\s*/).compact
253
+ excludes = excludes.map { |exclude| exclude =~ /:\d+$/ ? exclude : "#{exclude}:*" }
254
+ return proxy unless excludes.any? { |exclude| ::File.fnmatch(exclude, "#{host}:#{port}") }
255
+ end
256
+
257
+ def write_internal(options, &block) #:nodoc:
258
+ fail 'This protocol doesn\'t support writing (yet, how about helping by implementing it?)'
259
+ end
260
+
261
+ end
262
+
263
+
264
+ class HTTP #:nodoc:
265
+
266
+ # See URI::Generic#read
267
+ def read(options = nil, &block)
268
+ options ||= {}
269
+ connect do |http|
270
+ trace "Requesting #{self}"
271
+ headers = {}
272
+ headers['If-Modified-Since'] = CGI.rfc1123_date(options[:modified].utc) if options[:modified]
273
+ headers['Cache-Control'] = 'no-cache'
274
+ headers['User-Agent'] = "Buildr-#{Buildr::VERSION}"
275
+ request = Net::HTTP::Get.new(request_uri.empty? ? '/' : request_uri, headers)
276
+ request.basic_auth URI.decode(self.user), URI.decode(self.password) if self.user
277
+ http.verify_mode = ::OpenSSL::SSL.const_get(ENV['SSL_VERIFY_MODE']) if ENV['SSL_VERIFY_MODE']
278
+ http.ca_path = ENV['SSL_CA_CERTS'] if ENV['SSL_CA_CERTS']
279
+ http.request request do |response|
280
+ case response
281
+ when Net::HTTPNotModified
282
+ # No modification, nothing to do.
283
+ trace 'Not modified since last download'
284
+ return nil
285
+ when Net::HTTPRedirection
286
+ # Try to download from the new URI, handle relative redirects.
287
+ trace "Redirected to #{response['Location']}"
288
+ rself = self + URI.parse(response['Location'])
289
+ rself.user, rself.password = self.user, self.password
290
+ return rself.read(options, &block)
291
+ when Net::HTTPOK
292
+ info "Downloading #{self}"
293
+ result = nil
294
+ with_progress_bar options[:progress], path.split('/').last, response.content_length do |progress|
295
+ if block
296
+ response.read_body do |chunk|
297
+ block.call chunk
298
+ progress << chunk
299
+ end
300
+ else
301
+ result = ''
302
+ response.read_body do |chunk|
303
+ result << chunk
304
+ progress << chunk
305
+ end
306
+ end
307
+ end
308
+ return result
309
+ when Net::HTTPUnauthorized
310
+ raise NotFoundError, "Looking for #{self} but repository says Unauthorized/401."
311
+ when Net::HTTPNotFound
312
+ raise NotFoundError, "Looking for #{self} and all I got was a 404!"
313
+ else
314
+ raise RuntimeError, "Failed to download #{self}: #{response.message}"
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ private
321
+
322
+ def write_internal(options, &block) #:nodoc:
323
+ options ||= {}
324
+ connect do |http|
325
+ http.read_timeout = 500
326
+ trace "Uploading to #{path}"
327
+ content = StringIO.new
328
+ while chunk = yield(RW_CHUNK_SIZE)
329
+ content << chunk
330
+ end
331
+ headers = { 'Content-MD5'=>Digest::MD5.hexdigest(content.string), 'Content-Type'=>'application/octet-stream', 'User-Agent'=>"Buildr-#{Buildr::VERSION}" }
332
+ request = Net::HTTP::Put.new(request_uri.empty? ? '/' : request_uri, headers)
333
+ request.basic_auth URI.decode(self.user), URI.decode(self.password) if self.user
334
+ response = nil
335
+ with_progress_bar options[:progress], path.split('/').last, content.size do |progress|
336
+ request.content_length = content.size
337
+ content.rewind
338
+ stream = Object.new
339
+ class << stream ; self ;end.send :define_method, :read do |*args|
340
+ bytes = content.read(*args)
341
+ progress << bytes if bytes
342
+ bytes
343
+ end
344
+ request.body_stream = stream
345
+ response = http.request(request)
346
+ end
347
+
348
+ case response
349
+ when Net::HTTPRedirection
350
+ # Try to download from the new URI, handle relative redirects.
351
+ trace "Redirected to #{response['Location']}"
352
+ content.rewind
353
+ return (self + URI.parse(response['location'])).write_internal(options) { |bytes| content.read(bytes) }
354
+ when Net::HTTPSuccess
355
+ else
356
+ raise RuntimeError, "Failed to upload #{self}: #{response.message}"
357
+ end
358
+ end
359
+ end
360
+
361
+ def connect
362
+ if proxy = proxy_uri
363
+ proxy = URI.parse(proxy) if String === proxy
364
+ http = Net::HTTP.new(host, port, proxy.host, proxy.port, proxy.user, proxy.password)
365
+ else
366
+ http = Net::HTTP.new(host, port)
367
+ end
368
+ if self.instance_of? URI::HTTPS
369
+ require 'net/https'
370
+ http.use_ssl = true
371
+ end
372
+ yield http
373
+ end
374
+ end
375
+
376
+ # File URL. Keep in mind that file URLs take the form of <code>file://host/path</code>, although the host
377
+ # is not used, so typically all you will see are three backslashes. This methods accept common variants,
378
+ # like <code>file:/path</code> but always returns a valid URL.
379
+ class FILE < Generic
380
+ COMPONENT = [ :host, :path ].freeze
381
+
382
+ def upload(source, options = nil)
383
+ super
384
+ if ::File === source then
385
+ ::File.chmod(source.stat.mode, real_path)
386
+ end
387
+ end
388
+
389
+ def initialize(*args)
390
+ super
391
+ # file:something (opaque) becomes file:///something
392
+ if path.nil?
393
+ set_path "/#{opaque}"
394
+ unless opaque.nil?
395
+ set_opaque nil
396
+ warn "#{caller[2]}: We'll accept this URL, but just so you know, it needs three slashes, as in: #{to_s}"
397
+ end
398
+ end
399
+ # Sadly, file://something really means file://something/ (something being server)
400
+ set_path '/' if path.empty?
401
+
402
+ # On windows, file://c:/something is not a valid URL, but people do it anyway, so if we see a drive-as-host,
403
+ # we'll just be nice enough to fix it. (URI actually strips the colon here)
404
+ if host =~ /^[a-zA-Z]$/
405
+ set_path "/#{host}:#{path}"
406
+ set_host nil
407
+ end
408
+ end
409
+
410
+ # See URI::Generic#read
411
+ def read(options = nil, &block)
412
+ options ||= {}
413
+ raise ArgumentError, 'Either you\'re attempting to read a file from another host (which we don\'t support), or you used two slashes by mistake, where you should have file:///<path>.' if host
414
+
415
+ path = real_path
416
+ # TODO: complain about clunky URLs
417
+ raise NotFoundError, "Looking for #{self} and can't find it." unless ::File.exists?(path)
418
+ raise NotFoundError, "Looking for the file #{self}, and it happens to be a directory." if ::File.directory?(path)
419
+ ::File.open path, 'rb' do |input|
420
+ with_progress_bar options[:progress], path.split('/').last, input.stat.size do |progress|
421
+ block ? block.call(input.read) : input.read
422
+ end
423
+ end
424
+ end
425
+
426
+ def to_s
427
+ "file://#{host}#{path}"
428
+ end
429
+
430
+ # Returns the file system path based that corresponds to the URL path.
431
+ # On all platforms this method unescapes the URL path.
432
+ def real_path #:nodoc:
433
+ CGI.unescape(path)
434
+ end
435
+
436
+ protected
437
+
438
+ def write_internal(options, &block) #:nodoc:
439
+ raise ArgumentError, 'Either you\'re attempting to write a file to another host (which we don\'t support), or you used two slashes by mistake, where you should have file:///<path>.' if host
440
+ temp = Tempfile.new(::File.basename(path))
441
+ temp.binmode
442
+ with_progress_bar options[:progress] && options[:size], path.split('/').last, options[:size] || 0 do |progress|
443
+ while chunk = yield(RW_CHUNK_SIZE)
444
+ temp.write chunk
445
+ progress << chunk
446
+ end
447
+ end
448
+ temp.close
449
+ mkpath ::File.dirname(real_path)
450
+ mv temp.path, real_path
451
+ real_path
452
+ end
453
+
454
+ @@schemes['FILE'] = FILE
455
+ end
456
+ end