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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +176 -0
- data/NOTICE +26 -0
- data/README.md +3 -0
- data/Rakefile +50 -0
- data/addon/buildr/checkstyle-report.xsl +104 -0
- data/addon/buildr/checkstyle.rb +254 -0
- data/addon/buildr/git_auto_version.rb +36 -0
- data/addon/buildr/gpg.rb +90 -0
- data/addon/buildr/gwt.rb +413 -0
- data/addon/buildr/jacoco.rb +161 -0
- data/addon/buildr/pmd.rb +185 -0
- data/addon/buildr/single_intermediate_layout.rb +71 -0
- data/addon/buildr/spotbugs.rb +265 -0
- data/addon/buildr/top_level_generate_dir.rb +37 -0
- data/addon/buildr/wsgen.rb +192 -0
- data/bin/buildr +20 -0
- data/buildr.gemspec +61 -0
- data/lib/buildr.rb +86 -0
- data/lib/buildr/core/application.rb +705 -0
- data/lib/buildr/core/assets.rb +96 -0
- data/lib/buildr/core/build.rb +587 -0
- data/lib/buildr/core/common.rb +167 -0
- data/lib/buildr/core/compile.rb +599 -0
- data/lib/buildr/core/console.rb +124 -0
- data/lib/buildr/core/doc.rb +275 -0
- data/lib/buildr/core/environment.rb +128 -0
- data/lib/buildr/core/filter.rb +405 -0
- data/lib/buildr/core/help.rb +114 -0
- data/lib/buildr/core/progressbar.rb +161 -0
- data/lib/buildr/core/project.rb +994 -0
- data/lib/buildr/core/test.rb +776 -0
- data/lib/buildr/core/transports.rb +456 -0
- data/lib/buildr/core/util.rb +77 -0
- data/lib/buildr/ide/idea.rb +1664 -0
- data/lib/buildr/java/commands.rb +230 -0
- data/lib/buildr/java/compiler.rb +85 -0
- data/lib/buildr/java/custom_pom.rb +300 -0
- data/lib/buildr/java/doc.rb +62 -0
- data/lib/buildr/java/packaging.rb +393 -0
- data/lib/buildr/java/pom.rb +191 -0
- data/lib/buildr/java/test_result.rb +54 -0
- data/lib/buildr/java/tests.rb +111 -0
- data/lib/buildr/packaging/archive.rb +586 -0
- data/lib/buildr/packaging/artifact.rb +1113 -0
- data/lib/buildr/packaging/artifact_namespace.rb +1010 -0
- data/lib/buildr/packaging/artifact_search.rb +138 -0
- data/lib/buildr/packaging/package.rb +237 -0
- data/lib/buildr/packaging/version_requirement.rb +189 -0
- data/lib/buildr/packaging/zip.rb +189 -0
- data/lib/buildr/packaging/ziptask.rb +387 -0
- data/lib/buildr/version.rb +18 -0
- data/rakelib/release.rake +99 -0
- data/spec/addon/checkstyle_spec.rb +58 -0
- data/spec/core/application_spec.rb +576 -0
- data/spec/core/build_spec.rb +922 -0
- data/spec/core/common_spec.rb +670 -0
- data/spec/core/compile_spec.rb +656 -0
- data/spec/core/console_spec.rb +65 -0
- data/spec/core/doc_spec.rb +194 -0
- data/spec/core/extension_spec.rb +200 -0
- data/spec/core/project_spec.rb +736 -0
- data/spec/core/test_spec.rb +1131 -0
- data/spec/core/transport_spec.rb +452 -0
- data/spec/core/util_spec.rb +154 -0
- data/spec/ide/idea_spec.rb +1952 -0
- data/spec/java/commands_spec.rb +79 -0
- data/spec/java/compiler_spec.rb +274 -0
- data/spec/java/custom_pom_spec.rb +165 -0
- data/spec/java/doc_spec.rb +55 -0
- data/spec/java/packaging_spec.rb +786 -0
- data/spec/java/pom_spec.rb +162 -0
- data/spec/java/test_coverage_helper.rb +257 -0
- data/spec/java/tests_spec.rb +224 -0
- data/spec/packaging/archive_spec.rb +686 -0
- data/spec/packaging/artifact_namespace_spec.rb +757 -0
- data/spec/packaging/artifact_spec.rb +1351 -0
- data/spec/packaging/packaging_helper.rb +63 -0
- data/spec/packaging/packaging_spec.rb +690 -0
- data/spec/sandbox.rb +166 -0
- data/spec/spec_helpers.rb +420 -0
- data/spec/version_requirement_spec.rb +145 -0
- data/spec/xpath_matchers.rb +123 -0
- 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
|