vanagon 0.9.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 79f3affb2f2797cad077ac7341d99cc2f613dd96
4
- data.tar.gz: 4a90613f59d7e96c9015e61ff0200404f063fb3d
3
+ metadata.gz: f50d333d7dc08205687fc7058b19a3580a0b5028
4
+ data.tar.gz: c345a2510f5d6fb4d522a680da59f2aebaa274f3
5
5
  SHA512:
6
- metadata.gz: 0f6b28cd90a8917063035914d5a094ce123aad6991aa721df89bab0c8bd9251e9aafbe924c5253b9c1ea8a6d046fa20149870b448496d126a0d354dd7ebee925
7
- data.tar.gz: 6bc644ee0a223cebe705c0c43d7e69cc4de66b099dc05cfb436967298d21b194b320cb9d385e7beafb9132737eedd945268a93b7b8a75b45540940fdeae3a3c2
6
+ metadata.gz: 259aac01aa4832879c74a99e3578bc9d59d76e07e283729f78ebd71e4807650c508b306f7a1da13c2d7738b989cb0b40f9583ad629e0fe68ad828a35d74f421e
7
+ data.tar.gz: 7b72a6e2947e07cc178fd373cfc381e28f84259715bfa260362334dedda3a6e99a4fe56f1f228ead0f7bbc74d8cdcc41b25e2be9086b55e530bc3a6b30e1d5c4
data/lib/makefile.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'vanagon/environment'
2
+
1
3
  class Makefile
2
4
  # The Rule class defines a single Makefile rule.
3
5
  #
@@ -11,6 +13,11 @@ class Makefile
11
13
  # @return [Array<String>] A list of dependencies that this rule depends on.
12
14
  attr_accessor :dependencies
13
15
 
16
+ # @!attribute [rw] environment
17
+ # @return [Array<String>] A list of environment variables that this rule
18
+ # will export
19
+ attr_accessor :environment
20
+
14
21
  # @!attribute [rw] recipe
15
22
  # @return [Array<String>] A list of commands to execute upon invocation of this rule.
16
23
  attr_accessor :recipe
@@ -36,28 +43,84 @@ class Makefile
36
43
  # "make cpplint",
37
44
  # ]
38
45
  # end
39
- def initialize(target, dependencies: [], recipe: [], &block)
46
+ def initialize(target, dependencies: [], environment: Vanagon::Environment.new, recipe: [], &block)
40
47
  @target = target
41
48
  @dependencies = dependencies
49
+ @environment = environment
42
50
  @recipe = recipe
43
51
 
44
52
  yield(self) if block
45
53
  end
46
54
 
55
+ # @return [String, Nil] the name of all dependencies for a given rule,
56
+ # flattened and joined for a Makefule target
57
+ def flatten_dependencies
58
+ return nil if dependencies.empty?
59
+ dependencies.flatten.join("\s")
60
+ end
61
+
62
+ # @return [String] the base Rule for a Makefile target, including
63
+ # all dependencies.
64
+ def base_target
65
+ ["#{target}:", dependencies].flatten.compact.join("\s")
66
+ end
67
+
68
+ # @return [String] the Makefile target's name, rendered in a format
69
+ # suitable for using as a Graphite group -- any periods in the name of
70
+ # the component being built will be removed.
71
+ # e.g. "ruby-2.1.9-unpack" will become "ruby-219.unpack"
72
+ def tokenize_target_name
73
+ target_name, _, rule = target.rpartition('-')
74
+ [target_name.tr('.', ''), rule]
75
+ .select { |s| !(s.nil? || s.empty?) }
76
+ .join('.')
77
+ end
78
+
79
+ def tokenized_environment_variable
80
+ "#{target}: export VANAGON_TARGET := #{tokenize_target_name}"
81
+ end
82
+
83
+ def environment_variables
84
+ environment.map { |k, v| "#{k} := #{v}" }.map do |env|
85
+ "#{target}: export #{env}"
86
+ end
87
+ end
88
+
47
89
  # Format this rule as a Makefile rule.
48
90
  #
49
91
  # Recipes that have multiline statements will have tabs inserted after each
50
92
  # newline to ensure that the recipe is parsed as part of a single makefile rule.
51
93
  #
52
94
  # @return [String]
53
- def format
54
- s = @target + ":"
55
- unless @dependencies.empty?
56
- s << " " << @dependencies.join(" ")
95
+ def format # rubocop:disable Metrics/AbcSize
96
+ # create a base target inside an Array, and construct the rest of
97
+ # the rule around that.
98
+ t = [base_target]
99
+
100
+ # prepend an environment variable that can be used inside a
101
+ # given Make rule/target. We have to do it this way instead of
102
+ # appending it to #environment because for reasons that I cannot
103
+ # work out, the "sane" way results in previous/incorrect names
104
+ # being used and objects being recycled. My working theory is
105
+ # a corner case between metaprogrammed methods in Component::Rules,
106
+ # and Ruby's preference for pass-by-reference.
107
+ # Ryan McKern 2017-02-02
108
+ t.unshift tokenized_environment_variable
109
+
110
+ # prepend any environment variables to the existing target,
111
+ # using the target prefix to identify them as such. they should
112
+ # end up ahead of the dependencies and the build recipe.
113
+ environment_variables.each do |env|
114
+ t.unshift env
57
115
  end
58
- s << "\n"
59
- s << @recipe.map { |line| "\t" + line.gsub("\n", "\n\t") + "\n" }.join
60
- s
116
+
117
+ # finally, append the build recipe after the base target condition.
118
+ # also, here's a fun edge case: if one were to call #squeeze on
119
+ # the iterator 'line', basically all of the phony make tasks that
120
+ # `touch` a file just disapear. Fragility ++.
121
+ # - Ryan McKern 2017-02-02
122
+ t.push recipe.compact.map { |line| "\t" + line.gsub("\n", "\n\t") + "\n" }.join
123
+ t.join("\n")
61
124
  end
62
125
 
63
126
  alias to_s format
@@ -7,12 +7,101 @@ class Vanagon
7
7
  # @!attribute [r] files
8
8
  # @return [Set] the list of files marked for installation
9
9
 
10
- attr_accessor :name, :version, :source, :url, :configure, :build, :check, :install
11
- attr_accessor :environment, :extract_with, :dirname, :build_requires, :build_dir
12
- attr_accessor :settings, :platform, :patches, :requires, :service, :options
13
- attr_accessor :directories, :replaces, :provides, :conflicts, :cleanup_source
14
- attr_accessor :sources, :preinstall_actions, :postinstall_actions
15
- attr_accessor :preremove_actions, :postremove_actions, :license
10
+ # 30 accessors is too many. These have got to be refactored.
11
+ # - Ryan McKern, 2017-01-27
12
+
13
+ # The name, version, primary source, supplementary sources,
14
+ # associated patches, upstream URL, and license of a given component
15
+ attr_accessor :name
16
+ attr_accessor :version
17
+ attr_accessor :source
18
+ attr_accessor :sources
19
+ attr_accessor :patches
20
+ attr_accessor :url
21
+ attr_accessor :license
22
+
23
+ # holds an OpenStruct describing all of the particular details about
24
+ # how any services associated with a given component should be defined.
25
+ attr_accessor :service
26
+
27
+ # holds the expected directory name of a given component, once it's
28
+ # been unpacked/decompressed. For git repos, it's usually the directory
29
+ # that they were cloned to. For the outlying flat files, it'll
30
+ # end up being defined explicitly as the string './'
31
+ attr_accessor :dirname
32
+ # what special tool should be used to extract the primary source
33
+ attr_accessor :extract_with
34
+ # how should this component be configured?
35
+ attr_accessor :configure
36
+ # the optional name of a directory to build a component in; most
37
+ # likely to be used for cmake projects, which do not like to be
38
+ # configured or compiled in their own top-level directories.
39
+ attr_accessor :build_dir
40
+ # build will hold an Array of the commands required to build
41
+ # a given component
42
+ attr_accessor :build
43
+ # check will hold an Array of the commands required to validate/test
44
+ # a given component
45
+ attr_accessor :check
46
+ # install will hold an Array of the commands required to install
47
+ # a given component
48
+ attr_accessor :install
49
+
50
+ # holds a Vanagon::Environment object, to map out any desired
51
+ # environment variables that should be rendered into the Makefile
52
+ attr_accessor :environment
53
+ # holds a OpenStruct, or an Array, or maybe it's a Hash? It's often
54
+ # overloaded as a freeform key-value lookup for platforms that require
55
+ # additional configuration beyond the "basic" component attributes.
56
+ # it's pretty heavily overloaded and should maybe be refactored before
57
+ # Vanagon 1.0.0 is tagged.
58
+ attr_accessor :settings
59
+ # used to hold the checksum settings or other weirdo metadata related
60
+ # to building a given component (git ref, sha, etc.). Probably conflicts
61
+ # or collides with #settings to some degree.
62
+ attr_accessor :options
63
+ # the platform that a given component will be built for -- due to the
64
+ # fact that Ruby is pass-by-reference, it's usually just a reference
65
+ # to the same Platform object that the overall Project object also
66
+ # contains. This is a definite code smell, and should be slated
67
+ # for refactoring ASAP because it's going to have weird side-effects
68
+ # if the underlying pass-by-reference assumptions change.
69
+ attr_accessor :platform
70
+
71
+ # directories holds an Array with a list of expected directories that will
72
+ # be packed into the resulting artifact's bill of materials.
73
+ attr_accessor :directories
74
+ # build_requires holds an Array with a list of the dependencies that a given
75
+ # component needs satisfied before it can be built.
76
+ attr_accessor :build_requires
77
+ # requires holds an Array with a list of all dependencies that a given
78
+ # component needs satisfied before it can be installed.
79
+ attr_accessor :requires
80
+ # replaces holds an Array of OpenStructs that describe a package that a given
81
+ # component will replace on installation.
82
+ attr_accessor :replaces
83
+ # provides holds an Array of OpenStructs that describe any capabilities that
84
+ # a given component will provide beyond the its filesystem payload.
85
+ attr_accessor :provides
86
+ # conflicts holds an Array of OpenStructs that describe a package that a
87
+ # given component will replace on installation.
88
+ attr_accessor :conflicts
89
+ # preinstall_actions is a two-dimensional Array, describing scripts that
90
+ # should be executed before a given component is installed.
91
+ attr_accessor :preinstall_actions
92
+ # postinstall_actions is a two-dimensional Array, describing scripts that
93
+ # should be executed after a given component is installed.
94
+ attr_accessor :postinstall_actions
95
+ # preremove_actions is a two-dimensional Array, describing scripts that
96
+ # should be executed before a given component is uninstalled.
97
+ attr_accessor :preremove_actions
98
+ # preinstall_actions is a two-dimensional Array, describing scripts that
99
+ # should be executed after a given component is uninstalled.
100
+ attr_accessor :postremove_actions
101
+ # cleanup_source contains whatever value a given component's Source has
102
+ # specified as instructions for cleaning up after a build is completed.
103
+ # usually a String, but not required to be.
104
+ attr_accessor :cleanup_source
16
105
 
17
106
  # Loads a given component from the configdir
18
107
  #
@@ -58,7 +147,7 @@ class Vanagon
58
147
  @replaces = []
59
148
  @provides = []
60
149
  @conflicts = []
61
- @environment = {}
150
+ @environment = Vanagon::Environment.new
62
151
  @sources = []
63
152
  @preinstall_actions = []
64
153
  @postinstall_actions = []
@@ -111,7 +200,7 @@ class Vanagon
111
200
  @source = Vanagon::Component::Source.source(url, opts)
112
201
  source.fetch
113
202
  source.verify
114
- @extract_with = source.respond_to?(:extract) ? source.extract(platform.tar) : ':'
203
+ @extract_with = source.respond_to?(:extract) ? source.extract(platform.tar) : nil
115
204
  @cleanup_source = source.cleanup if source.respond_to?(:cleanup)
116
205
  @dirname = source.dirname
117
206
 
@@ -126,7 +215,7 @@ class Vanagon
126
215
  @dirname = './'
127
216
 
128
217
  # If there is no source, there is nothing to do to extract
129
- @extract_with = ':'
218
+ @extract_with = ': no source, so nothing to extract'
130
219
  end
131
220
  end
132
221
 
@@ -144,10 +233,9 @@ class Vanagon
144
233
  # @param workdir [String] working directory to put the source into
145
234
  def get_sources(workdir)
146
235
  sources.each do |source|
147
- src = Vanagon::Component::Source.source source.url,
148
- workdir: workdir,
149
- ref: source.ref,
150
- sum: source.sum
236
+ src = Vanagon::Component::Source.source(
237
+ source.url, workdir: workdir, ref: source.ref, sum: source.sum
238
+ )
151
239
  src.fetch
152
240
  src.verify
153
241
  end
@@ -165,12 +253,20 @@ class Vanagon
165
253
  end
166
254
 
167
255
  # Prints the environment in a way suitable for use in a Makefile
168
- # or shell script.
256
+ # or shell script. This is deprecated, because all Env. Vars. are
257
+ # moving directly into the Makefile (and out of recipe subshells).
169
258
  #
170
259
  # @return [String] environment suitable for inclusion in a Makefile
260
+ # @deprecated
171
261
  def get_environment
262
+ warn <<-eos.undent
263
+ #get_environment is deprecated; environment variables have been moved
264
+ into the Makefile, and should not be used within a Makefile's recipe.
265
+ The #get_environment method will be removed by Vanagon 1.0.0.
266
+ eos
267
+
172
268
  if @environment.empty?
173
- ":"
269
+ ": no environment variables defined"
174
270
  else
175
271
  env = @environment.map { |key, value| %(#{key}="#{value}") }
176
272
  "export #{env.join(' ')}"
@@ -230,14 +230,14 @@ class Vanagon
230
230
  # upgrade if it has been modified
231
231
  #
232
232
  # @param file [String] name of the configfile
233
- def configfile(file, mode: nil, owner: nil, group: nil)
233
+ def configfile(file)
234
234
  # I AM SO SORRY
235
235
  @component.delete_file file
236
236
  if @component.platform.name =~ /solaris-10|osx/
237
237
  @component.install << "mv '#{file}' '#{file}.pristine'"
238
- @component.add_file Vanagon::Common::Pathname.configfile("#{file}.pristine", mode: mode, owner: owner, group: group)
238
+ @component.add_file Vanagon::Common::Pathname.configfile("#{file}.pristine")
239
239
  else
240
- @component.add_file Vanagon::Common::Pathname.configfile(file, mode: mode, owner: owner, group: group)
240
+ @component.add_file Vanagon::Common::Pathname.configfile(file)
241
241
  end
242
242
  end
243
243
 
@@ -245,9 +245,9 @@ class Vanagon
245
245
  #
246
246
  # @param source [String] path to the configfile to copy
247
247
  # @param target [String] path to the desired target of the configfile
248
- def install_configfile(source, target, mode: '0644', owner: nil, group: nil)
249
- install_file(source, target, mode: mode, owner: owner, group: group)
250
- configfile(target, mode: mode, owner: owner, group: group)
248
+ def install_configfile(source, target)
249
+ install_file(source, target)
250
+ configfile(target)
251
251
  end
252
252
 
253
253
  # link will add a command to the install to create a symlink from source to target
@@ -339,8 +339,25 @@ class Vanagon
339
339
  # This environment is included in the configure, build and install steps.
340
340
  #
341
341
  # @param env [Hash] mapping of keys to values to add to the environment for the component
342
- def environment(env)
343
- @component.environment.merge!(env)
342
+ def environment(*env) # rubocop:disable Metrics/AbcSize
343
+ if env.size == 1 && env.first.is_a?(Hash)
344
+ warn <<-eos.undent
345
+ the component DSL method signature #environment({Key => Value}) is deprecated
346
+ and will be removed by Vanagon 1.0.0.
347
+
348
+ Please update your project configurations to use the form:
349
+ #environment(key, value)
350
+ eos
351
+ return @component.environment.merge!(env.first)
352
+ elsif env.size == 2
353
+ return @component.environment[env.key] = env.value
354
+ end
355
+ raise ArgumentError, <<-eos.undent
356
+ component DSL method #environment only accepts a single Hash (deprecated)
357
+ or a key-value pair (preferred):
358
+ environment({"KEY" => "value"})
359
+ environment("KEY", "value")
360
+ eos
344
361
  end
345
362
 
346
363
  # Adds action to run during the preinstall phase of packaging
@@ -20,14 +20,18 @@ class Vanagon
20
20
  #
21
21
  # @!macro [attach] rule
22
22
  # @return [Makefile::Rule] The $1 rule
23
- def self.rule(target, dependencies: [], &block)
23
+ def self.rule(target, &block)
24
24
  define_method("#{target}_rule") do
25
- Makefile::Rule.new("#{@component.name}-#{target}", dependencies: dependencies) do |rule|
25
+ Makefile::Rule.new("#{component.name}-#{target}", environment: component.environment) do |rule|
26
26
  instance_exec(rule, &block)
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
+ attr_accessor :component
32
+ attr_accessor :project
33
+ attr_accessor :platform
34
+
31
35
  # @param component [Vanagon::Component] The component to create rules for.
32
36
  # @param project [Vanagon::Project] The project associated with the component.
33
37
  # @param platform [Vanagon::Platform] The platform where this component will be built.
@@ -55,7 +59,7 @@ class Vanagon
55
59
  clean_rule,
56
60
  clobber_rule,
57
61
  ]
58
- if @project.cleanup
62
+ if project.cleanup
59
63
  list << cleanup_rule
60
64
  end
61
65
 
@@ -66,27 +70,29 @@ class Vanagon
66
70
  #
67
71
  # @return [Makefile::Rule]
68
72
  def component_rule
69
- Makefile::Rule.new(@component.name, dependencies: ["#{@component.name}-install"])
73
+ Makefile::Rule.new(component.name, environment: component.environment) do |rule|
74
+ rule.dependencies = ["#{component.name}-install"]
75
+ end
70
76
  end
71
77
 
72
78
  # Unpack the source for this component. The unpacking behavior depends on
73
79
  # the source type of the component.
74
80
  #
75
81
  # @see [Vanagon::Component::Source]
76
- rule("unpack", dependencies: ['file-list-before-build']) do |r|
77
- r.recipe << andand(@component.get_environment, @component.extract_with)
82
+ rule("unpack") do |r|
83
+ r.dependencies = ['file-list-before-build']
84
+ r.recipe << component.extract_with
78
85
  r.recipe << "touch #{r.target}"
79
86
  end
80
87
 
81
88
  # Apply any patches for this component.
82
89
  rule("patch") do |r|
83
- r.dependencies = ["#{@component.name}-unpack"]
84
-
85
- after_unpack_patches = @component.patches.select { |patch| patch.after == "unpack" }
90
+ r.dependencies = ["#{component.name}-unpack"]
91
+ after_unpack_patches = component.patches.select { |patch| patch.after == "unpack" }
86
92
  unless after_unpack_patches.empty?
87
93
  r.recipe << andand_multiline(
88
- "cd #{@component.dirname}",
89
- after_unpack_patches.map { |patch| patch.cmd(@platform) }
94
+ "cd #{component.dirname}",
95
+ after_unpack_patches.map { |patch| patch.cmd(platform) }
90
96
  )
91
97
  end
92
98
 
@@ -96,17 +102,15 @@ class Vanagon
96
102
  # Create a build directory for this component if an out of source tree build is specified,
97
103
  # and any configure steps, if any.
98
104
  rule("configure") do |r|
99
- r.dependencies = ["#{@component.name}-patch"].concat(@project.list_component_dependencies(@component))
100
-
101
- if @component.get_build_dir
102
- r.recipe << "[ -d #{@component.get_build_dir} ] || mkdir -p #{@component.get_build_dir}"
105
+ r.dependencies = ["#{component.name}-patch"].concat(project.list_component_dependencies(component))
106
+ if component.get_build_dir
107
+ r.recipe << "[ -d #{component.get_build_dir} ] || mkdir -p #{component.get_build_dir}"
103
108
  end
104
109
 
105
- unless @component.configure.empty?
110
+ unless component.configure.empty?
106
111
  r.recipe << andand_multiline(
107
- "cd #{@component.get_build_dir}",
108
- @component.get_environment,
109
- @component.configure
112
+ "cd #{component.get_build_dir}",
113
+ component.configure
110
114
  )
111
115
  end
112
116
 
@@ -115,13 +119,11 @@ class Vanagon
115
119
 
116
120
  # Build this component.
117
121
  rule("build") do |r|
118
- r.dependencies = ["#{@component.name}-configure"]
119
-
120
- unless @component.build.empty?
122
+ r.dependencies = ["#{component.name}-configure"]
123
+ unless component.build.empty?
121
124
  r.recipe << andand_multiline(
122
- "cd #{@component.get_build_dir}",
123
- @component.get_environment,
124
- @component.build
125
+ "cd #{component.get_build_dir}",
126
+ component.build
125
127
  )
126
128
  end
127
129
 
@@ -130,13 +132,11 @@ class Vanagon
130
132
 
131
133
  # Run tests for this component.
132
134
  rule("check") do |r|
133
- r.dependencies = ["#{@component.name}-build"]
134
-
135
- unless @component.check.empty? || @project.settings[:skipcheck]
135
+ r.dependencies = ["#{component.name}-build"]
136
+ unless component.check.empty? || project.settings[:skipcheck]
136
137
  r.recipe << andand_multiline(
137
- "cd #{@component.get_build_dir}",
138
- @component.get_environment,
139
- @component.check
138
+ "cd #{component.get_build_dir}",
139
+ component.check
140
140
  )
141
141
  end
142
142
 
@@ -145,21 +145,19 @@ class Vanagon
145
145
 
146
146
  # Install this component.
147
147
  rule("install") do |r|
148
- r.dependencies = ["#{@component.name}-check"]
149
-
150
- unless @component.install.empty?
148
+ r.dependencies = ["#{component.name}-check"]
149
+ unless component.install.empty?
151
150
  r.recipe << andand_multiline(
152
- "cd #{@component.get_build_dir}",
153
- @component.get_environment,
154
- @component.install
151
+ "cd #{component.get_build_dir}",
152
+ component.install
155
153
  )
156
154
  end
157
155
 
158
- after_install_patches = @component.patches.select { |patch| patch.after == "install" }
156
+ after_install_patches = component.patches.select { |patch| patch.after == "install" }
159
157
  after_install_patches.each do |patch|
160
158
  r.recipe << andand(
161
159
  "cd #{patch.destination}",
162
- patch.cmd(@platform),
160
+ patch.cmd(platform),
163
161
  )
164
162
  end
165
163
 
@@ -171,8 +169,8 @@ class Vanagon
171
169
  # This component is only included by {#rules} if the associated project has
172
170
  # the `cleanup` attribute set.
173
171
  rule("cleanup") do |r|
174
- r.dependencies = ["#{@component.name}-install"]
175
- r.recipe = [@component.cleanup_source, "touch #{r.target}"]
172
+ r.dependencies = ["#{component.name}-install"]
173
+ r.recipe = [component.cleanup_source, "touch #{r.target}"]
176
174
  end
177
175
 
178
176
  # Clean up any files generated while building this project.
@@ -181,13 +179,13 @@ class Vanagon
181
179
  # for the configure/build/install steps.
182
180
  rule("clean") do |r|
183
181
  r.recipe << andand(
184
- "[ -d #{@component.get_build_dir} ]",
185
- "cd #{@component.get_build_dir}",
186
- "#{@platform[:make]} clean"
182
+ "[ -d #{component.get_build_dir} ]",
183
+ "cd #{component.get_build_dir}",
184
+ "#{platform[:make]} clean"
187
185
  )
188
186
 
189
187
  %w(configure build install).each do |type|
190
- touchfile = "#{@component.name}-#{type}"
188
+ touchfile = "#{component.name}-#{type}"
191
189
  r.recipe << andand(
192
190
  "[ -e #{touchfile} ]",
193
191
  "rm #{touchfile}"
@@ -197,10 +195,10 @@ class Vanagon
197
195
 
198
196
  # Remove all files associated with this component.
199
197
  rule("clobber") do |r|
200
- r.dependencies = ["#{@component.name}-clean"]
198
+ r.dependencies = ["#{component.name}-clean"]
201
199
  r.recipe = [
202
- andand("[ -d #{@component.dirname} ]", "rm -r #{@component.dirname}"),
203
- andand("[ -e #{@component.name}-unpack ]", "rm #{@component.name}-unpack")
200
+ andand("[ -d #{component.dirname} ]", "rm -r #{component.dirname}"),
201
+ andand("[ -e #{component.name}-unpack ]", "rm #{component.name}-unpack")
204
202
  ]
205
203
  end
206
204