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,191 @@
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
+ #
17
+ module Buildr
18
+ class POM
19
+
20
+ POM_TO_SPEC_MAP = { :group=> 'groupId', :id=> 'artifactId', :type=> 'type',
21
+ :version=> 'version', :classifier=> 'classifier', :scope=> 'scope'}
22
+ SCOPES_TRANSITIVE = [nil, 'compile', 'runtime']
23
+ SCOPES_WE_USE = SCOPES_TRANSITIVE + ['provided']
24
+
25
+ # POM project as Hash (using XmlSimple).
26
+ attr_reader :project
27
+ # Parent POM if referenced by this POM.
28
+ attr_reader :parent
29
+
30
+ class << self
31
+
32
+ # :call-seq:
33
+ # POM.load(arg)
34
+ #
35
+ # Load new POM object form various kind of sources such as artifact, hash representing spec, filename, XML.
36
+ def load(source)
37
+ case source
38
+ when Hash
39
+ load(Buildr.artifact(source).pom)
40
+ when Artifact
41
+ pom = source.pom
42
+ pom.invoke
43
+ load(pom.to_s)
44
+ when Rake::FileTask
45
+ source.invoke
46
+ load(source.to_s)
47
+ when String
48
+ filename = File.expand_path(source)
49
+ unless pom = cache[filename]
50
+ trace "Loading m2 pom file from #{filename}"
51
+ begin
52
+ pom = POM.new(IO.read(filename))
53
+ rescue REXML::ParseException => e
54
+ fail "Could not parse #{filename}, #{e.continued_exception}"
55
+ end
56
+ cache[filename] = pom
57
+ end
58
+ pom
59
+ else
60
+ raise ArgumentError, 'Expecting Hash spec, Artifact, file name or file task'
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def cache()
67
+ @cache ||= {}
68
+ end
69
+
70
+ end
71
+
72
+ def initialize(xml) #:nodoc:
73
+ @project = XmlSimple.xml_in(xml)
74
+ @parent = POM.load(pom_to_hash(project["parent"].first).merge(:type=>'pom')) if project['parent']
75
+ end
76
+
77
+ # :call-seq:
78
+ # dependencies(scopes?) => artifacts
79
+ # dependencies(:scopes = [:runtime, :test, ...], :optional = true) => artifacts
80
+ #
81
+ # Returns list of required dependencies as specified by the POM. You can specify which scopes
82
+ # to use (e.g. "compile", "runtime"); use +nil+ for dependencies with unspecified scope.
83
+ # The default scopes are +nil+, "compile" and "runtime" (aka SCOPES_WE_USE) and no optional dependencies.
84
+ # Specifying optional = true will return all optional dependencies matching the given scopes.
85
+ def dependencies(options = {})
86
+ # backward compatibility
87
+ options = { :scopes => options } if Array === options
88
+
89
+ # support symbols, but don't fidget with nil
90
+ options[:scopes] = (options[:scopes] || SCOPES_WE_USE).map { |s| s.to_s if s }
91
+
92
+ # try to cache dependencies also
93
+ @depends_for_scopes ||= {}
94
+ unless depends = @depends_for_scopes[options]
95
+ declared = project['dependencies'].first['dependency'] rescue nil
96
+ depends = (declared || [])
97
+ depends = depends.reject { |dep| value_of(dep['optional']) =~ /true/ } unless options[:optional]
98
+ depends = depends.map { |dep|
99
+ spec = pom_to_hash(dep, properties)
100
+ apply = managed(spec)
101
+ spec = apply.merge(spec) if apply
102
+
103
+ next if options[:exclusions] && options[:exclusions].any? { |ex| dep['groupId'] == ex['groupId'] && dep['artifactId'] == ex['artifactId'] }
104
+
105
+ # calculate transitive dependencies
106
+ if options[:scopes].include?(spec[:scope])
107
+ spec.delete(:scope)
108
+
109
+ exclusions = dep['exclusions'].first['exclusion'] rescue nil
110
+ transitive_deps = POM.load(spec).dependencies(:exclusions => exclusions, :scopes => (options[:scopes_transitive] || SCOPES_TRANSITIVE) ) rescue []
111
+
112
+ [Artifact.to_spec(spec)] + transitive_deps
113
+ end
114
+ }.flatten.compact #.uniq_by{|spec| art = spec.split(':'); "#{art[0]}:#{art[1]}"}
115
+ @depends_for_scopes[options] = depends
116
+ end
117
+ depends
118
+ end
119
+
120
+ # :call-seq:
121
+ # properties() => hash
122
+ #
123
+ # Returns properties available to this POM as hash. Includes explicit properties and pom.xxx/project.xxx
124
+ # properties for groupId, artifactId, version and packaging.
125
+ def properties()
126
+ @properties ||= begin
127
+ pom = %w(groupId artifactId version packaging).inject({}) { |hash, key|
128
+ value = project[key] || (parent ? parent.project[key] : nil)
129
+ hash[key] = hash["pom.#{key}"] = hash["project.#{key}"] = value_of(value) if value
130
+ hash
131
+ }
132
+ pom = %w(groupId artifactId version).inject(pom) { |hash, key|
133
+ value = parent.project[key]
134
+ hash[key] = hash["pom.parent.#{key}"] = hash["project.parent.#{key}"] = value_of(value) if value
135
+ hash
136
+ } if parent
137
+ props = project['properties'].first rescue {}
138
+ props = props.inject({}) { |mapped, pair| mapped[pair.first] = value_of(pair.last, props) ; mapped }
139
+ (parent ? parent.properties.merge(props) : props).merge(pom)
140
+ end
141
+ end
142
+
143
+ # :call-seq:
144
+ # managed() => hash
145
+ # managed(hash) => hash
146
+ #
147
+ # The first form returns all the managed dependencies specified by this POM in dependencyManagement.
148
+ # The second form uses a single spec hash and expands it from the current/parent POM. Used to determine
149
+ # the version number if specified in dependencyManagement instead of dependencies.
150
+ def managed(spec = nil)
151
+ if spec
152
+ managed.detect { |dep| [:group, :id, :type, :classifier].all? { |key| spec[key] == dep[key] } } ||
153
+ (parent ? parent.managed(spec) : nil)
154
+ else
155
+ @managed ||= begin
156
+ managed = project['dependencyManagement'].first['dependencies'].first['dependency'] rescue nil
157
+ managed ? managed.map { |dep| pom_to_hash(dep, properties) } : []
158
+ end
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ # :call-seq:
165
+ # value_of(element) => string
166
+ # value_of(element, true) => string
167
+ #
168
+ # Returns the normalized text value of an element from its XmlSimple value. The second form performs
169
+ # property substitution.
170
+ def value_of(element, substitute = nil)
171
+ value = element.to_a.join.strip
172
+ value = value.gsub(/\$\{([^}]+)\}/) { |key| Array(substitute[$1]).join.strip } if substitute
173
+ value
174
+ end
175
+
176
+ # :call-seq:
177
+ # pom_to_hash(element) => hash
178
+ # pom_to_hash(element, true) => hash
179
+ #
180
+ # Return the spec hash from an XmlSimple POM referencing element (e.g. project, parent, dependency).
181
+ # The second form performs property substitution.
182
+ def pom_to_hash(element, substitute = nil)
183
+ hash = POM_TO_SPEC_MAP.inject({}) { |spec, pair|
184
+ spec[pair.first] = value_of(element[pair.last], substitute) if element[pair.last]
185
+ spec
186
+ }
187
+ {:scope => 'compile', :type => 'jar'}.merge(hash)
188
+ end
189
+
190
+ end
191
+ end
@@ -0,0 +1,54 @@
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 'yaml'
17
+
18
+ module Buildr #:nodoc:
19
+ module TestFramework #:nodoc:
20
+
21
+ # A class used by buildr for jruby based frameworks, so that buildr can know
22
+ # which tests succeeded/failed.
23
+ class TestResult
24
+
25
+ class Error < ::Exception
26
+ attr_reader :message, :backtrace
27
+ def initialize(message, backtrace)
28
+ @message = message
29
+ @backtrace = backtrace
30
+ set_backtrace backtrace
31
+ end
32
+
33
+ def self.dump_yaml(file, e)
34
+ FileUtils.mkdir_p File.dirname(file)
35
+ File.open(file, 'w') { |f| f.puts(YAML.dump(Error.new(e.message, e.backtrace))) }
36
+ end
37
+
38
+ def self.guard(file)
39
+ begin
40
+ yield
41
+ rescue => e
42
+ dump_yaml(file, e)
43
+ end
44
+ end
45
+ end
46
+
47
+ attr_accessor :failed, :succeeded
48
+
49
+ def initialize
50
+ @failed, @succeeded = [], []
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,111 @@
1
+
2
+ # Licensed to the Apache Software Foundation (ASF) under one or more
3
+ # contributor license agreements. See the NOTICE file distributed with this
4
+ # work for additional information regarding copyright ownership. The ASF
5
+ # licenses this file to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ # License for the specific language governing permissions and limitations under
15
+ # the License.
16
+
17
+ module Buildr #:nodoc:
18
+
19
+ class TestFramework::Java < TestFramework::Base
20
+
21
+ class << self
22
+
23
+ def applies_to?(project) #:nodoc:
24
+ project.test.compile.language == :java
25
+ end
26
+
27
+ def dependencies
28
+ super
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def derive_test_candidates
35
+ return [] unless task.compile.target
36
+ target = task.compile.target.to_s
37
+ Dir["#{target}/**/*.class"].
38
+ map { |file| Util.relative_path(file, target).ext('').gsub(File::SEPARATOR, '.') }.
39
+ reject { |name| name =~ /\$./ }
40
+ end
41
+ end
42
+
43
+ # TestNG test framework. To use in your project:
44
+ # test.using :testng
45
+ #
46
+ # Support the following options:
47
+ # * :properties -- Hash of properties passed to the test suite.
48
+ # * :java_args -- Arguments passed to the JVM.
49
+ # * :args -- Arguments passed to the TestNG command line runner.
50
+ class TestNG < TestFramework::Java
51
+
52
+ class << self
53
+ def dependencies
54
+ %w(org.testng:testng:jar:7.4.0 com.beust:jcommander:jar:1.78 org.webjars:jquery:jar:3.5.1)
55
+ end
56
+ end
57
+
58
+ def tests(dependencies) #:nodoc:
59
+ candidates = derive_test_candidates
60
+
61
+ # Ugly hack that probably works for all of our codebases
62
+ test_include = /.*Test$/
63
+ test_exclude = /(^|\.)Abstract[^.]*$/
64
+ candidates.select{|c| c =~ test_include }.select{|c| !(c =~ test_exclude) }.dup
65
+ end
66
+
67
+ def run(tests, dependencies) #:nodoc:
68
+ cmd_args = []
69
+ cmd_args << '-suitename' << task.project.id
70
+ cmd_args << '-log' << '2'
71
+ cmd_args << '-d' << task.report_to.to_s
72
+ exclude_args = options[:excludegroups] || []
73
+ unless exclude_args.empty?
74
+ cmd_args << '-excludegroups' << exclude_args.join(',')
75
+ end
76
+ groups_args = options[:groups] || []
77
+ unless groups_args.empty?
78
+ cmd_args << '-groups' << groups_args.join(',')
79
+ end
80
+ # run all tests in the same suite
81
+ cmd_args << '-testclass' << tests.join(',')
82
+
83
+ cmd_args += options[:args] if options[:args]
84
+
85
+ cmd_options = { :properties=>options[:properties], :java_args=>options[:java_args],
86
+ :classpath=>dependencies, :name => "TestNG in #{task.send(:project).name}" }
87
+
88
+ tmp = nil
89
+ begin
90
+ tmp = Tempfile.open('testNG')
91
+ tmp.write cmd_args.join("\n")
92
+ tmp.close
93
+ Java::Commands.java ['org.testng.TestNG', "@#{tmp.path}"], cmd_options
94
+ ensure
95
+ tmp.close unless tmp.nil?
96
+ end
97
+ # testng-failed.xml contains the list of failed tests *only*
98
+ failed_tests = File.join(task.report_to.to_s, 'testng-failed.xml')
99
+ if File.exist?(failed_tests)
100
+ report = File.read(failed_tests)
101
+ failed = report.scan(/<class name="(.*?)">/im).flatten
102
+ # return the list of passed tests
103
+ tests - failed
104
+ else
105
+ tests
106
+ end
107
+ end
108
+ end
109
+ end # Buildr
110
+
111
+ Buildr::TestFramework << Buildr::TestNG
@@ -0,0 +1,586 @@
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
+ module Buildr #:nodoc:
17
+
18
+ # Base class for ZipTask, TarTask and other archives.
19
+ class ArchiveTask < Rake::FileTask
20
+
21
+ # Which files go where. All the rules for including, excluding and merging files
22
+ # are handled by this object.
23
+ class Path #:nodoc:
24
+
25
+ # Returns the archive from this path.
26
+ attr_reader :root
27
+
28
+ def initialize(root, path)
29
+ @root = root
30
+ @path = path.empty? ? path : "#{path}/"
31
+ @includes = FileList[]
32
+ @excludes = []
33
+ # Expand source files added to this path.
34
+ expand_src = proc { @includes.map{ |file| file.to_s }.uniq }
35
+ @sources = [ expand_src ]
36
+ # Add files and directories added to this path.
37
+ @actions = [] << proc do |file_map|
38
+ expand_src.call.each do |path|
39
+ unless excluded?(path)
40
+ if File.directory?(path)
41
+ in_directory path do |file, rel_path|
42
+ dest = "#{@path}#{rel_path}"
43
+ unless excluded?(dest)
44
+ trace "Adding #{dest}"
45
+ file_map[dest] = file
46
+ end
47
+ end
48
+ end
49
+ unless File.basename(path) == "."
50
+ trace "Adding #{@path}#{File.basename(path)}"
51
+ file_map["#{@path}#{File.basename(path)}"] = path
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # :call-seq:
59
+ # include(*files) => self
60
+ # include(*files, :path=>path) => self
61
+ # include(file, :as=>name) => self
62
+ # include(:from=>path) => self
63
+ # include(*files, :merge=>true) => self
64
+ def include(*args)
65
+ options = Hash === args.last ? args.pop : nil
66
+ files = to_artifacts(args)
67
+ raise 'AchiveTask.include() values should not include nil' if files.include? nil
68
+
69
+ if options.nil? || options.empty?
70
+ @includes.include *files.flatten
71
+ elsif options[:path]
72
+ sans_path = options.reject { |k,v| k == :path }
73
+ path(options[:path]).include *files + [sans_path]
74
+ elsif options[:as]
75
+ raise 'You can only use the :as option in combination with the :path option' unless options.size == 1
76
+ raise 'You can only use one file with the :as option' unless files.size == 1
77
+ include_as files.first.to_s, options[:as]
78
+ elsif options[:from]
79
+ raise 'You can only use the :from option in combination with the :path option' unless options.size == 1
80
+ raise 'You cannot use the :from option with file names' unless files.empty?
81
+ fail 'AchiveTask.include() :from value should not be nil' if [options[:from]].flatten.include? nil
82
+ [options[:from]].flatten.each { |path| include_as path.to_s, '.' }
83
+ elsif options[:merge]
84
+ raise 'You can only use the :merge option in combination with the :path option' unless options.size == 1
85
+ files.each { |file| merge file }
86
+ else
87
+ raise "Unrecognized option #{options.keys.join(', ')}"
88
+ end
89
+ self
90
+ end
91
+ alias :add :include
92
+ alias :<< :include
93
+
94
+ # :call-seq:
95
+ # exclude(*files) => self
96
+ def exclude(*files)
97
+ files = to_artifacts(files)
98
+ @excludes |= files
99
+ @excludes |= files.reject { |f| f =~ /\*$/ }.map { |f| "#{f}/*" }
100
+ self
101
+ end
102
+
103
+ # :call-seq:
104
+ # merge(*files) => Merge
105
+ # merge(*files, :path=>name) => Merge
106
+ def merge(*args)
107
+ options = Hash === args.last ? args.pop : {}
108
+ files = to_artifacts(args)
109
+ rake_check_options options, :path
110
+ raise ArgumentError, "Expected at least one file to merge" if files.empty?
111
+ path = options[:path] || @path
112
+ expanders = files.collect do |file|
113
+ @sources << proc { file.to_s }
114
+ expander = ZipExpander.new(file)
115
+ @actions << proc do |file_map, transform_map|
116
+ file.invoke() if file.is_a?(Rake::Task)
117
+ expander.expand(file_map, transform_map, path)
118
+ end
119
+ expander
120
+ end
121
+ Merge.new(expanders)
122
+ end
123
+
124
+ # Returns a Path relative to this one.
125
+ def path(path)
126
+ return self if path.nil?
127
+ return root.path(path[1..-1]) if path[0] == ?/
128
+ root.path("#{@path}#{path}")
129
+ end
130
+
131
+ # Returns all the source files.
132
+ def sources #:nodoc:
133
+ @sources.map{ |source| source.call }.flatten
134
+ end
135
+
136
+ def add_files(file_map, transform_map) #:nodoc:
137
+ @actions.each { |action| action.call(file_map, transform_map) }
138
+ end
139
+
140
+ # :call-seq:
141
+ # exist => boolean
142
+ #
143
+ # Returns true if this path exists. This only works if the path has any entries in it,
144
+ # so exist on path happens to be the opposite of empty.
145
+ def exist?
146
+ !entries.empty?
147
+ end
148
+
149
+ # :call-seq:
150
+ # empty? => boolean
151
+ #
152
+ # Returns true if this path is empty (has no other entries inside).
153
+ def empty?
154
+ entries.all? { |entry| entry.empty? }
155
+ end
156
+
157
+ # :call-seq:
158
+ # contain(file*) => boolean
159
+ #
160
+ # Returns true if this ZIP file path contains all the specified files. You can use relative
161
+ # file names and glob patterns (using *, **, etc).
162
+ def contain?(*files)
163
+ files.all? { |file| entries.detect { |entry| File.fnmatch(file, entry.to_s) } }
164
+ end
165
+
166
+ # :call-seq:
167
+ # entry(name) => ZipEntry
168
+ #
169
+ # Returns a ZIP file entry. You can use this to check if the entry exists and its contents,
170
+ # for example:
171
+ # package(:jar).path("META-INF").entry("LICENSE").should contain(/Apache Software License/)
172
+ def entry(name)
173
+ root.entry("#{@path}#{name}")
174
+ end
175
+
176
+ def to_s
177
+ @path
178
+ end
179
+
180
+ protected
181
+
182
+ # Convert objects to artifacts, where applicable
183
+ def to_artifacts(files)
184
+ files.flatten.inject([]) do |set, file|
185
+ case file
186
+ when ArtifactNamespace
187
+ set |= file.artifacts
188
+ when Symbol, Hash
189
+ set |= [Buildr.artifact(file)]
190
+ when /([^:]+:){2,4}/ # A spec as opposed to a file name.
191
+ set |= [Buildr.artifact(file)]
192
+ when Project
193
+ set |= Buildr.artifacts(file.packages)
194
+ when Rake::Task
195
+ set |= [file]
196
+ when Struct
197
+ set |= Buildr.artifacts(file.values)
198
+ else
199
+ # non-artifacts passed as-is; in particular, String paths are
200
+ # unmodified since Rake FileTasks don't use absolute paths
201
+ set |= [file]
202
+ end
203
+ end
204
+ end
205
+
206
+ def include_as(source, as)
207
+ @sources << proc { source }
208
+ @actions << proc do |file_map|
209
+ file = source.to_s
210
+ file(file).invoke
211
+ unless excluded?(file)
212
+ if File.directory?(file)
213
+ in_directory file do |file, rel_path|
214
+ path = rel_path.split('/')[1..-1]
215
+ path.unshift as unless as == '.'
216
+ dest = "#{@path}#{path.join('/')}"
217
+ unless excluded?(dest)
218
+ trace "Adding #{dest}"
219
+ file_map[dest] = file
220
+ end
221
+ end
222
+ unless as == "."
223
+ trace "Adding #{@path}#{as}/"
224
+ file_map["#{@path}#{as}/"] = nil # :as is a folder, so the trailing / is required.
225
+ end
226
+ else
227
+ file_map["#{@path}#{as}"] = file
228
+ end
229
+
230
+ end
231
+ end
232
+ end
233
+
234
+ def in_directory(dir)
235
+ prefix = Regexp.new('^' + Regexp.escape(File.dirname(dir) + File::SEPARATOR))
236
+ Util.recursive_with_dot_files(dir).reject { |file| excluded?(file) }.
237
+ each { |file| yield file, file.sub(prefix, '') }
238
+ end
239
+
240
+ def excluded?(file)
241
+ @excludes.any? { |exclude| File.fnmatch(exclude, file) }
242
+ end
243
+
244
+ def entries #:nodoc:
245
+ return root.entries unless @path
246
+ @entries ||= root.entries.inject([]) { |selected, entry|
247
+ selected << entry.name.sub(@path, "") if entry.name.index(@path) == 0
248
+ selected
249
+ }
250
+ end
251
+
252
+ end
253
+
254
+
255
+ class Merge
256
+ def initialize(expanders)
257
+ @expanders = expanders
258
+ end
259
+
260
+ def include(*files)
261
+ @expanders.each { |expander| expander.include(*files) }
262
+ self
263
+ end
264
+ alias :<< :include
265
+
266
+ def exclude(*files)
267
+ @expanders.each { |expander| expander.exclude(*files) }
268
+ self
269
+ end
270
+
271
+ def concatenate(*files)
272
+ @expanders.each { |expander| expander.concatenate(*files) }
273
+ self
274
+ end
275
+
276
+ def transform(*files, &block)
277
+ @expanders.each { |expander| expander.transform(*files, &block) }
278
+ self
279
+ end
280
+ end
281
+
282
+
283
+ # Extend one Zip file into another.
284
+ class ZipExpander #:nodoc:
285
+
286
+ def initialize(zip_file)
287
+ @zip_file = zip_file.to_s
288
+ @includes = []
289
+ @excludes = []
290
+ @concatenates = []
291
+ @transforms = {}
292
+ end
293
+
294
+ def include(*files)
295
+ @includes |= files
296
+ self
297
+ end
298
+ alias :<< :include
299
+
300
+ def exclude(*files)
301
+ @excludes |= files
302
+ self
303
+ end
304
+
305
+ def concatenate(*files)
306
+ @concatenates |= files
307
+ self
308
+ end
309
+
310
+ def transform(*files, &block)
311
+ @transforms[[files].flatten] = block
312
+ self
313
+ end
314
+
315
+ def expand(file_map, transform_map, path)
316
+ @includes = ['*'] if @includes.empty?
317
+ Zip::File.open(@zip_file) do |source|
318
+ source.entries.reject { |entry| entry.directory? }.each do |entry|
319
+ if @includes.any? { |pattern| File.fnmatch(pattern, entry.name) } &&
320
+ !@excludes.any? { |pattern| File.fnmatch(pattern, entry.name) }
321
+ dest = path =~ /^\/?$/ ? entry.name : Util.relative_path(path + "/" + entry.name)
322
+ trace "Adding #{dest}"
323
+ if @concatenates.any? { |pattern| File.fnmatch(pattern, entry.name) }
324
+ file_map[dest] << ZipEntryData.new(source, entry)
325
+ elsif @transforms.each_pair.detect do |transform, transform_block|\
326
+ if transform.any? { |pattern| File.fnmatch(pattern, entry.name) }
327
+ file_map[dest] << ZipEntryData.new(source, entry)
328
+
329
+ transform_map[dest] = transform_block
330
+ true
331
+ end
332
+ end
333
+ else
334
+ file_map[dest] = ZipEntryData.new(source, entry)
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
340
+
341
+ end
342
+
343
+ class ZipEntryData
344
+ def initialize(zipfile, entry)
345
+ @zipfile = zipfile
346
+ @entry = entry
347
+ end
348
+
349
+ def call(output)
350
+ output.write @zipfile.read(@entry)
351
+ end
352
+
353
+ def mode
354
+ @entry.unix_perms
355
+ end
356
+ end
357
+
358
+ def initialize(*args) #:nodoc:
359
+ super
360
+ clean
361
+
362
+ # Make sure we're the last enhancements, so other enhancements can add content.
363
+ enhance do
364
+ @file_map = Hash.new {|h,k| h[k]=[]}
365
+ @transform_map = {}
366
+ enhance do
367
+ send 'create' if respond_to?(:create)
368
+ # We're here because the archive file does not exist, or one of the files is newer than the archive contents;
369
+ # we need to make sure the archive doesn't exist (e.g. opening an existing Zip will add instead of create).
370
+ # We also want to protect against partial updates.
371
+ rm name rescue nil
372
+ mkpath File.dirname(name)
373
+ begin
374
+ @paths.each do |name, object|
375
+ @file_map[name] = nil unless name.empty?
376
+ object.add_files(@file_map, @transform_map)
377
+ end
378
+ create_from @file_map, @transform_map
379
+ rescue
380
+ rm name rescue nil
381
+ raise
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ # :call-seq:
388
+ # clean => self
389
+ #
390
+ # Removes all previously added content from this archive.
391
+ # Use this method if you want to remove default content from a package.
392
+ # For example, package(:jar) by default includes compiled classes and resources,
393
+ # using this method, you can create an empty jar and afterwards add the
394
+ # desired content to it.
395
+ #
396
+ # package(:jar).clean.include path_to('desired/content')
397
+ def clean
398
+ @paths = {}
399
+ @paths[''] = Path.new(self, '')
400
+ @prepares = []
401
+ self
402
+ end
403
+
404
+ # :call-seq:
405
+ # include(*files) => self
406
+ # include(*files, :path=>path) => self
407
+ # include(file, :as=>name) => self
408
+ # include(:from=>path) => self
409
+ # include(*files, :merge=>true) => self
410
+ #
411
+ # Include files in this archive, or when called on a path, within that path. Returns self.
412
+ #
413
+ # The first form accepts a list of files, directories and glob patterns and adds them to the archive.
414
+ # For example, to include the file foo, directory bar (including all files in there) and all files under baz:
415
+ # zip(..).include('foo', 'bar', 'baz/*')
416
+ #
417
+ # The second form is similar but adds files/directories under the specified path. For example,
418
+ # to add foo as bar/foo:
419
+ # zip(..).include('foo', :path=>'bar')
420
+ # The :path option is the same as using the path method:
421
+ # zip(..).path('bar').include('foo')
422
+ # All other options can be used in combination with the :path option.
423
+ #
424
+ # The third form adds a file or directory under a different name. For example, to add the file foo under the
425
+ # name bar:
426
+ # zip(..).include('foo', :as=>'bar')
427
+ #
428
+ # The fourth form adds the contents of a directory using the directory as a prerequisite:
429
+ # zip(..).include(:from=>'foo')
430
+ # Unlike <code>include('foo')</code> it includes the contents of the directory, not the directory itself.
431
+ # Unlike <code>include('foo/*')</code>, it uses the directory timestamp for dependency management.
432
+ #
433
+ # The fifth form includes the contents of another archive by expanding it into this archive. For example:
434
+ # zip(..).include('foo.zip', :merge=>true).include('bar.zip')
435
+ # You can also use the method #merge.
436
+ def include(*files)
437
+ fail "AchiveTask.include() called with nil values" if files.include? nil
438
+ @paths[''].include *files if files.compact.size > 0
439
+ self
440
+ end
441
+ alias :add :include
442
+ alias :<< :include
443
+
444
+ # :call-seq:
445
+ # exclude(*files) => self
446
+ #
447
+ # Excludes files and returns self. Can be used in combination with include to prevent some files from being included.
448
+ def exclude(*files)
449
+ @paths[''].exclude *files
450
+ self
451
+ end
452
+
453
+ # :call-seq:
454
+ # merge(*files) => Merge
455
+ # merge(*files, :path=>name) => Merge
456
+ #
457
+ # Merges another archive into this one by including the individual files from the merged archive.
458
+ #
459
+ # Returns an object that supports two methods: include and exclude. You can use these methods to merge
460
+ # only specific files. For example:
461
+ # zip(..).merge('src.zip').include('module1/*')
462
+ def merge(*files)
463
+ @paths[''].merge *files
464
+ end
465
+
466
+ # :call-seq:
467
+ # path(name) => Path
468
+ #
469
+ # Returns a path object. Use the path object to include files under a path, for example, to include
470
+ # the file 'foo' as 'bar/foo':
471
+ # zip(..).path('bar').include('foo')
472
+ #
473
+ # Returns a Path object. The Path object implements all the same methods, like include, exclude, merge
474
+ # and so forth. It also implements path and root, so that:
475
+ # path('foo').path('bar') == path('foo/bar')
476
+ # path('foo').root == root
477
+ def path(name)
478
+ return @paths[''] if name.nil?
479
+ normalized = name.split('/').inject([]) do |path, part|
480
+ case part
481
+ when '.', nil, ''
482
+ path
483
+ when '..'
484
+ path[0...-1]
485
+ else
486
+ path << part
487
+ end
488
+ end.join('/')
489
+ @paths[normalized] ||= Path.new(self, normalized)
490
+ end
491
+
492
+ # :call-seq:
493
+ # root => ArchiveTask
494
+ #
495
+ # Call this on an archive to return itself, and on a path to return the archive.
496
+ def root
497
+ self
498
+ end
499
+
500
+ # :call-seq:
501
+ # with(options) => self
502
+ #
503
+ # Passes options to the task and returns self. Some tasks support additional options, for example,
504
+ # the WarTask supports options like :manifest, :libs and :classes.
505
+ #
506
+ # For example:
507
+ # package(:jar).with(:manifest=>'MANIFEST_MF')
508
+ def with(options)
509
+ options.each do |key, value|
510
+ begin
511
+ send "#{key}=", value
512
+ rescue NoMethodError
513
+ raise ArgumentError, "#{self.class.name} does not support the option #{key}"
514
+ end
515
+ end
516
+ self
517
+ end
518
+
519
+ def invoke_prerequisites(args, chain) #:nodoc:
520
+ @prepares.each { |prepare| prepare.call(self) }
521
+ @prepares.clear
522
+
523
+ file_map = Hash.new {|h,k| h[k]=[]}
524
+ transform_map = {}
525
+ @paths.each do |name, path|
526
+ path.add_files(file_map, transform_map)
527
+ end
528
+
529
+ # filter out Procs (dynamic content), nils and others
530
+ @prerequisites |= file_map.values.select { |src| src.is_a?(String) || src.is_a?(Rake::Task) }
531
+
532
+ super
533
+ end
534
+
535
+ def needed? #:nodoc:
536
+ return true unless File.exist?(name)
537
+ # You can do something like:
538
+ # include('foo', :path=>'foo').exclude('foo/bar', path=>'foo').
539
+ # include('foo/bar', :path=>'foo/bar')
540
+ # This will play havoc if we handled all the prerequisites together
541
+ # under the task, so instead we handle them individually for each path.
542
+ #
543
+ # We need to check that any file we include is not newer than the
544
+ # contents of the Zip. The file itself but also the directory it's
545
+ # coming from, since some tasks touch the directory, e.g. when the
546
+ # content of target/classes is included into a WAR.
547
+ most_recent = @paths.collect { |name, path| path.sources }.flatten.
548
+ select { |file| File.exist?(file) }.collect { |file| File.stat(file).mtime }.max
549
+ File.stat(name).mtime < (most_recent || Rake::EARLY) || super
550
+ end
551
+
552
+ # :call-seq:
553
+ # empty? => boolean
554
+ #
555
+ # Returns true if this ZIP file is empty (has no other entries inside).
556
+ def empty?
557
+ path("").empty
558
+ end
559
+
560
+ # :call-seq:
561
+ # contain(file*) => boolean
562
+ #
563
+ # Returns true if this ZIP file contains all the specified files. You can use absolute
564
+ # file names and glob patterns (using *, **, etc).
565
+ def contain?(*files)
566
+ path("").contain?(*files)
567
+ end
568
+
569
+ protected
570
+
571
+ # Adds a prepare block. These blocks are called early on for adding more content to
572
+ # the archive, before invoking prerequsities. Anything you add here will be invoked
573
+ # as a prerequisite and used to determine whether or not to generate this archive.
574
+ # In contrast, enhance blocks are evaluated after it was decided to create this archive.
575
+ def prepare(&block)
576
+ @prepares << block
577
+ end
578
+
579
+ def []=(key, value) #:nodoc:
580
+ raise ArgumentError, "This task does not support the option #{key}."
581
+ end
582
+
583
+ end
584
+
585
+
586
+ end