chbuild 0.0.2
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/.editorconfig +15 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +9 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +7 -0
- data/bin/chbuild +167 -0
- data/bin/console +8 -0
- data/bin/setup +8 -0
- data/chbuild.gemspec +45 -0
- data/lib/chbuild.rb +8 -0
- data/lib/chbuild/bindable_hash.rb +16 -0
- data/lib/chbuild/config.rb +74 -0
- data/lib/chbuild/config/before.rb +30 -0
- data/lib/chbuild/config/env.rb +29 -0
- data/lib/chbuild/config/errors.rb +7 -0
- data/lib/chbuild/config/use.rb +42 -0
- data/lib/chbuild/config/version.rb +34 -0
- data/lib/chbuild/constants.rb +25 -0
- data/lib/chbuild/controller.rb +230 -0
- data/lib/chbuild/utils.rb +42 -0
- data/lib/chbuild/version.rb +4 -0
- data/templates/base-docker-container.erb +104 -0
- data/templates/init.sh.erb +19 -0
- metadata +216 -0
data/bin/console
ADDED
data/bin/setup
ADDED
data/chbuild.gemspec
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require_relative 'lib/chbuild/version.rb'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'chbuild'
|
9
|
+
spec.version = CHBuild::VERSION
|
10
|
+
spec.author = 'Cheppers Ltd.'
|
11
|
+
spec.email = 'info@cheppers.com'
|
12
|
+
spec.summary = 'Cheppers Build Tool'
|
13
|
+
spec.homepage = 'https://github.com/Cheppers/chbuild'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
spec.description = <<-EOS
|
16
|
+
chbuild is an open-source command line application written in Ruby.
|
17
|
+
It's main purpose is to simplify web app development by running a site
|
18
|
+
using Docker containers so you don't have to install Apache, PHP and
|
19
|
+
PHP extensions on your own machine.
|
20
|
+
EOS
|
21
|
+
|
22
|
+
spec.required_ruby_version = '>= 2.3.0'
|
23
|
+
|
24
|
+
if spec.respond_to?(:metadata)
|
25
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
26
|
+
else
|
27
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
|
28
|
+
end
|
29
|
+
|
30
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) }
|
31
|
+
spec.bindir = 'bin'
|
32
|
+
spec.executables = ['chbuild']
|
33
|
+
spec.require_paths = ['lib']
|
34
|
+
|
35
|
+
spec.add_dependency 'colorize', '~> 0.8'
|
36
|
+
spec.add_dependency 'excon', '~> 0.46', '>= 0.46'
|
37
|
+
spec.add_dependency 'docker-api', '~> 1.31', '>= 1.31'
|
38
|
+
spec.add_dependency 'thor', '~> 0.19'
|
39
|
+
|
40
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
41
|
+
spec.add_development_dependency 'pry', '~> 0.10'
|
42
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
43
|
+
spec.add_development_dependency 'rubocop', '~> 0.40'
|
44
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
45
|
+
end
|
data/lib/chbuild.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# CHBuild module :D
|
3
|
+
module CHBuild
|
4
|
+
# BindableHash is for ERB templates
|
5
|
+
class BindableHash
|
6
|
+
def initialize(hash)
|
7
|
+
hash.each do |key, value|
|
8
|
+
singleton_class.send(:define_method, key) { value }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_binding # rubocop:disable Style/AccessorMethodName
|
13
|
+
binding
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'chbuild/config/before'
|
3
|
+
require 'chbuild/config/env'
|
4
|
+
require 'chbuild/config/errors'
|
5
|
+
require 'chbuild/config/use'
|
6
|
+
require 'chbuild/config/version'
|
7
|
+
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
module CHBuild
|
11
|
+
# Build configuration object
|
12
|
+
class Config
|
13
|
+
attr_reader :path, :raw, :version, :use, :env, :before
|
14
|
+
|
15
|
+
def initialize(path_to_config)
|
16
|
+
begin
|
17
|
+
@path = path_to_config
|
18
|
+
build_config = YAML.load_file(path_to_config)
|
19
|
+
rescue Errno::ENOENT
|
20
|
+
raise CHBuild::Config::NotFoundError, "'#{path_to_config}': file not found"
|
21
|
+
end
|
22
|
+
|
23
|
+
@errors = []
|
24
|
+
unless build_config
|
25
|
+
@errors << 'Build file is empty'
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
@raw = build_config
|
30
|
+
@version = Version.new(build_config['version'])
|
31
|
+
@use = Use.new(build_config['use'])
|
32
|
+
@env = Env.new(build_config['env'])
|
33
|
+
@before = Before.new(build_config['before'])
|
34
|
+
end
|
35
|
+
|
36
|
+
def init_script
|
37
|
+
unless @init_script
|
38
|
+
generation_time = "echo \"Generated at: [#{Time.now}]\"\n\n"
|
39
|
+
env_script = @env.to_bash_script
|
40
|
+
before_script = @before.to_bash_script
|
41
|
+
@init_script = generation_time + env_script + before_script
|
42
|
+
end
|
43
|
+
@init_script
|
44
|
+
end
|
45
|
+
|
46
|
+
def errors
|
47
|
+
all_errors = Array.new(@errors)
|
48
|
+
|
49
|
+
instance_variables.each do |var|
|
50
|
+
section = instance_variable_get(var)
|
51
|
+
next unless section.respond_to?(:name) && section.respond_to?(:errors)
|
52
|
+
section_name = section.send(:name)
|
53
|
+
section_errors = section.send(:errors)
|
54
|
+
section_errors.each do |err|
|
55
|
+
all_errors << "#{section_name}: #{err}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
all_errors
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
data = []
|
64
|
+
instance_variables.each do |var|
|
65
|
+
section = instance_variable_get(var)
|
66
|
+
if section.respond_to?(:name)
|
67
|
+
section_name = section.send(:name)
|
68
|
+
data << "#{section_name}: #{section.inspect}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
data.join("\n")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module CHBuild
|
3
|
+
class Config
|
4
|
+
# Before section
|
5
|
+
class Before < Array
|
6
|
+
# before_commands is required so no default value
|
7
|
+
def initialize(before_commands)
|
8
|
+
super([])
|
9
|
+
validate!(before_commands)
|
10
|
+
replace(before_commands) unless before_commands.nil?
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate!(before_commands)
|
14
|
+
@errors = []
|
15
|
+
@errors << 'Required' if before_commands.nil?
|
16
|
+
@errors << 'Cannot be empty' if before_commands == []
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :errors
|
20
|
+
|
21
|
+
def name
|
22
|
+
"Section 'before'"
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_bash_script
|
26
|
+
reduce('') { |a, e| a + "#{e}\n" }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module CHBuild
|
3
|
+
class Config
|
4
|
+
# Env section
|
5
|
+
class Env < Hash
|
6
|
+
def initialize(env = {})
|
7
|
+
validate!(env)
|
8
|
+
super
|
9
|
+
replace(env) unless env.nil?
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate!(_env)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def errors
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
"Section 'env'"
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_bash_script
|
25
|
+
reduce('') { |a, (k, v)| a + "export #{k}=\"#{v}\"\n" }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chbuild/constants'
|
4
|
+
|
5
|
+
module CHBuild
|
6
|
+
class Config
|
7
|
+
# Use section
|
8
|
+
class Use < Hash
|
9
|
+
DEFAULTS ||= {
|
10
|
+
'php' => CHBuild::DEFAULT_PHP_VERSION,
|
11
|
+
'mysql' => CHBuild::DEFAULT_MYSQL_VERSION
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
ALLOWED_VALUES ||= DEFAULTS.keys
|
15
|
+
|
16
|
+
def initialize(using = {})
|
17
|
+
super
|
18
|
+
replace(DEFAULTS)
|
19
|
+
|
20
|
+
validate!(using)
|
21
|
+
|
22
|
+
merge!(using) unless using.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate!(using)
|
26
|
+
@errors = []
|
27
|
+
if using.respond_to? :keys
|
28
|
+
extra_keys = using.keys - ALLOWED_VALUES
|
29
|
+
extra_keys.each do |key|
|
30
|
+
@errors << "Unknown key: #{key}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_reader :errors
|
36
|
+
|
37
|
+
def name
|
38
|
+
"Section 'use'"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module CHBuild
|
3
|
+
class Config
|
4
|
+
# Version section
|
5
|
+
class Version
|
6
|
+
ALLOWED_VALUES ||= [1.0].freeze
|
7
|
+
|
8
|
+
# version is required so no default value
|
9
|
+
def initialize(version)
|
10
|
+
validate!(version)
|
11
|
+
@yaml_version = version
|
12
|
+
end
|
13
|
+
|
14
|
+
def inspect
|
15
|
+
@yaml_version.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!(version)
|
19
|
+
@errors = []
|
20
|
+
if version.nil?
|
21
|
+
@errors << 'Required'
|
22
|
+
return
|
23
|
+
end
|
24
|
+
@errors << "Unknown value: '#{version}'" unless ALLOWED_VALUES.include? version
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :errors
|
28
|
+
|
29
|
+
def name
|
30
|
+
"Section 'version'"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
# CHBuild main module
|
6
|
+
module CHBuild
|
7
|
+
GEM_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '../../'))
|
8
|
+
TEMPLATE_DIR = "#{CHBuild::GEM_ROOT}/templates"
|
9
|
+
DEFAULT_DOMAIN = 'cheppers.com'
|
10
|
+
|
11
|
+
IMAGE_NAME ||= 'chbuild'
|
12
|
+
DOCKER_DIR ||= 'docker'
|
13
|
+
DEFAULT_PHP_VERSION = '5.6'
|
14
|
+
DEFAULT_MYSQL_VERSION = 'latest'
|
15
|
+
|
16
|
+
DEFAULT_OPTS ||= {
|
17
|
+
refreshed_at_date: ::Date.today.strftime('%Y-%m-%d'),
|
18
|
+
php_timezone: 'Europe/Budapest',
|
19
|
+
php_memory_limit: '256M',
|
20
|
+
max_upload: '50M',
|
21
|
+
php_max_file_upload: 200,
|
22
|
+
php_max_post: '100M',
|
23
|
+
extra_files: CHBuild::TEMPLATE_DIR
|
24
|
+
}.freeze
|
25
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'docker-api'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
require 'chbuild/bindable_hash'
|
6
|
+
require 'chbuild/config'
|
7
|
+
require 'chbuild/utils'
|
8
|
+
|
9
|
+
# rubocop:disable Metrics/ClassLength, Lint/AssignmentInCondition
|
10
|
+
|
11
|
+
# CHBuild main module
|
12
|
+
module CHBuild
|
13
|
+
# CHBuild::Controller
|
14
|
+
class Controller
|
15
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
16
|
+
def self.config(path_to_config = nil)
|
17
|
+
# rubocop:disable Style/ClassVars
|
18
|
+
@@config ||= nil
|
19
|
+
|
20
|
+
unless @@config
|
21
|
+
@@config = if path_to_config.nil?
|
22
|
+
CHBuild::Config.new(File.join(Dir.pwd, '.chbuild.yml'))
|
23
|
+
else
|
24
|
+
CHBuild::Config.new(path_to_config)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@@config
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.image_exist?
|
31
|
+
Docker::Image.exist? CHBuild::IMAGE_NAME
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.container?
|
35
|
+
Docker::Container.all(
|
36
|
+
'filters' => { 'ancestor' => [CHBuild::IMAGE_NAME] }.to_json, 'all' => true
|
37
|
+
).first
|
38
|
+
rescue
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.promote
|
43
|
+
puts '!!! WIP !!!'
|
44
|
+
puts "FQDN: #{CHBuild::Utils.fqdn}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.build
|
48
|
+
template = load_docker_template(
|
49
|
+
'base-docker-container',
|
50
|
+
php_memory_limit: '128M'
|
51
|
+
)
|
52
|
+
|
53
|
+
docker_context = generate_docker_archive(template)
|
54
|
+
|
55
|
+
Docker::Image.build_from_tar(docker_context, t: CHBuild::IMAGE_NAME) do |r|
|
56
|
+
r.each_line do |log|
|
57
|
+
if (message = JSON.parse(log)) && message.key?('stream')
|
58
|
+
yield message['stream'] if block_given?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# rubocop:disable Metrics/PerceivedComplexity, Lint/UnusedMethodArgument, Metrics/MethodLength
|
65
|
+
def self.run(config_path: nil, webroot: nil, initscripts: nil)
|
66
|
+
unless image_exist?
|
67
|
+
yield "Image doesn't exist" if block_given?
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
|
71
|
+
bind_volumes = []
|
72
|
+
|
73
|
+
if !webroot.nil? && Dir.exist?(File.expand_path(webroot))
|
74
|
+
bind_volumes << "#{webroot}:/www"
|
75
|
+
end
|
76
|
+
if !initscripts.nil? && Dir.exist?(File.expand_path(initscripts))
|
77
|
+
bind_volumes << "#{initscripts}:/initscripts"
|
78
|
+
end
|
79
|
+
|
80
|
+
if c = container?
|
81
|
+
yield 'Removing existing chbuild container...' if block_given?
|
82
|
+
c.remove(force: true)
|
83
|
+
end
|
84
|
+
|
85
|
+
mysql_container_version = "mysql:#{config.use['mysql']}"
|
86
|
+
unless Docker::Image.exist? mysql_container_version
|
87
|
+
yield "Downloading #{mysql_container_version}..." if block_given?
|
88
|
+
Docker::Image.create('fromImage' => mysql_container_version)
|
89
|
+
end
|
90
|
+
|
91
|
+
mysql_container_name = mysql_container_version.tr(':.', '_')
|
92
|
+
|
93
|
+
stop_mysql_containers(except: mysql_container_version)
|
94
|
+
begin
|
95
|
+
mysql_container = Docker::Container.get(mysql_container_name)
|
96
|
+
rescue Docker::Error::NotFoundError
|
97
|
+
yield 'Creating MySQL container...' if block_given?
|
98
|
+
mysql_container = Docker::Container.create(
|
99
|
+
'name' => mysql_container_name,
|
100
|
+
'Image' => mysql_container_version,
|
101
|
+
'Env' => ['MYSQL_ROOT_PASSWORD=admin']
|
102
|
+
)
|
103
|
+
ensure
|
104
|
+
mysql_container_info = Docker::Container.get(mysql_container.id).info
|
105
|
+
unless mysql_container_info['State']['Running']
|
106
|
+
yield 'Starting MySQL container...' if block_given?
|
107
|
+
mysql_container.start!
|
108
|
+
sleep 10
|
109
|
+
end
|
110
|
+
end
|
111
|
+
mysql_container_info = Docker::Container.get(mysql_container.id).info
|
112
|
+
unless mysql_container_info['State']['Running']
|
113
|
+
yield "Couldn't start MySQL container" if block_given?
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
yield "MySQL container: #{mysql_container.id[0, 10]}" if block_given?
|
117
|
+
|
118
|
+
yield 'Creating CHBuild container...' if block_given?
|
119
|
+
container = Docker::Container.create(
|
120
|
+
'name' => CHBuild::Utils.generate_conatiner_name,
|
121
|
+
'Image' => CHBuild::IMAGE_NAME,
|
122
|
+
'Cmd' => ['/init.sh'],
|
123
|
+
'Volumes' => {
|
124
|
+
"#{Dir.pwd}/webroot" => {},
|
125
|
+
"#{Dir.pwd}/initscripts" => {}
|
126
|
+
},
|
127
|
+
'ExposedPorts' => {
|
128
|
+
'80/tcp' => {}
|
129
|
+
},
|
130
|
+
'HostConfig' => {
|
131
|
+
'Links' => ["#{mysql_container_name}:mysql"],
|
132
|
+
'PortBindings' => {
|
133
|
+
'80/tcp' => [{ 'HostPort' => '8088' }]
|
134
|
+
},
|
135
|
+
'Binds' => bind_volumes
|
136
|
+
}
|
137
|
+
)
|
138
|
+
|
139
|
+
container.store_file('/init.sh', content: load_init_script, permissions: 0777)
|
140
|
+
|
141
|
+
container.start!
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.delete_container
|
145
|
+
if c = container?
|
146
|
+
c.delete(force: true)
|
147
|
+
true
|
148
|
+
else
|
149
|
+
false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.container_id
|
154
|
+
if c = container?
|
155
|
+
c.id[0, 10]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.container_logs
|
160
|
+
if c = container?
|
161
|
+
c.logs(stdout: true)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.delete_image
|
166
|
+
if image_exist?
|
167
|
+
image = Docker::Image.get CHBuild::IMAGE_NAME
|
168
|
+
image.remove(force: true)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def self.mysql_containers
|
173
|
+
Docker::Container.all.select { |c| c.info['Image'].start_with?('mysql') }
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.delete_mysql_containers
|
177
|
+
if mysql_containers.empty?
|
178
|
+
false
|
179
|
+
else
|
180
|
+
mysql_containers.each { |c| c.delete(force: true) }
|
181
|
+
true
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def self.stop_mysql_containers(except: "\n")
|
186
|
+
mysql_containers.select { |c| !c.info['Image'].end_with?(except) }.each(&:stop)
|
187
|
+
end
|
188
|
+
|
189
|
+
private_class_method
|
190
|
+
|
191
|
+
def self.load_docker_template(template_name, opts = {})
|
192
|
+
opts = CHBuild::DEFAULT_OPTS.merge(opts)
|
193
|
+
|
194
|
+
context = BindableHash.new opts
|
195
|
+
::ERB.new(
|
196
|
+
File.read("#{CHBuild::TEMPLATE_DIR}/#{template_name}.erb")
|
197
|
+
).result(context.get_binding)
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.load_init_script
|
201
|
+
context = BindableHash.new(before_script: config.init_script)
|
202
|
+
::ERB.new(
|
203
|
+
File.read("#{CHBuild::TEMPLATE_DIR}/init.sh.erb")
|
204
|
+
).result(context.get_binding)
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.generate_docker_archive(dockerfile_content)
|
208
|
+
tar = StringIO.new
|
209
|
+
|
210
|
+
Gem::Package::TarWriter.new(tar) do |writer|
|
211
|
+
writer.add_file('Dockerfile', 0644) { |f| f.write(dockerfile_content) }
|
212
|
+
end
|
213
|
+
|
214
|
+
compress_archive(tar)
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.compress_archive(tar)
|
218
|
+
tar.seek(0)
|
219
|
+
# rubocop:disable Style/EmptyLiteral
|
220
|
+
gz = StringIO.new(String.new, 'r+b').set_encoding(Encoding::BINARY)
|
221
|
+
gz_writer = Zlib::GzipWriter.new(gz)
|
222
|
+
gz_writer.write(tar.read)
|
223
|
+
tar.close
|
224
|
+
gz_writer.finish
|
225
|
+
gz.rewind
|
226
|
+
|
227
|
+
gz
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|