akabei 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +77 -0
- data/Rakefile +28 -0
- data/akabei.gemspec +2 -0
- data/lib/akabei/build_helper.rb +15 -0
- data/lib/akabei/builder.rb +7 -0
- data/lib/akabei/chroot_tree.rb +1 -0
- data/lib/akabei/cli.rb +18 -29
- data/lib/akabei/omakase/cli.rb +116 -0
- data/lib/akabei/omakase/config.rb +113 -0
- data/lib/akabei/omakase/s3.rb +71 -0
- data/lib/akabei/omakase/templates/.akabei.yml.tt +24 -0
- data/lib/akabei/omakase/templates/makepkg.i686.conf +140 -0
- data/lib/akabei/omakase/templates/makepkg.x86_64.conf +140 -0
- data/lib/akabei/omakase/templates/pacman.i686.conf +90 -0
- data/lib/akabei/omakase/templates/pacman.x86_64.conf +99 -0
- data/lib/akabei/repository.rb +5 -4
- data/lib/akabei/signer.rb +6 -2
- data/lib/akabei/version.rb +1 -1
- data/spec/akabei/builder_spec.rb +1 -5
- data/spec/akabei/cli_spec.rb +135 -0
- data/spec/akabei/omakase/cli_spec.rb +154 -0
- data/spec/akabei/repository_spec.rb +1 -2
- metadata +42 -2
@@ -0,0 +1,99 @@
|
|
1
|
+
#
|
2
|
+
# /etc/pacman.conf
|
3
|
+
#
|
4
|
+
# See the pacman.conf(5) manpage for option and repository directives
|
5
|
+
|
6
|
+
#
|
7
|
+
# GENERAL OPTIONS
|
8
|
+
#
|
9
|
+
[options]
|
10
|
+
# The following paths are commented out with their default values listed.
|
11
|
+
# If you wish to use different paths, uncomment and update the paths.
|
12
|
+
#RootDir = /
|
13
|
+
#DBPath = /var/lib/pacman/
|
14
|
+
#CacheDir = /var/cache/pacman/pkg/
|
15
|
+
#LogFile = /var/log/pacman.log
|
16
|
+
#GPGDir = /etc/pacman.d/gnupg/
|
17
|
+
HoldPkg = pacman glibc
|
18
|
+
#XferCommand = /usr/bin/curl -C - -f %u > %o
|
19
|
+
#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u
|
20
|
+
#CleanMethod = KeepInstalled
|
21
|
+
#UseDelta = 0.7
|
22
|
+
Architecture = auto
|
23
|
+
|
24
|
+
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
|
25
|
+
#IgnorePkg =
|
26
|
+
#IgnoreGroup =
|
27
|
+
|
28
|
+
#NoUpgrade =
|
29
|
+
#NoExtract =
|
30
|
+
|
31
|
+
# Misc options
|
32
|
+
#UseSyslog
|
33
|
+
#Color
|
34
|
+
#TotalDownload
|
35
|
+
CheckSpace
|
36
|
+
#VerbosePkgLists
|
37
|
+
|
38
|
+
# By default, pacman accepts packages signed by keys that its local keyring
|
39
|
+
# trusts (see pacman-key and its man page), as well as unsigned packages.
|
40
|
+
SigLevel = Required DatabaseOptional
|
41
|
+
LocalFileSigLevel = Optional
|
42
|
+
#RemoteFileSigLevel = Required
|
43
|
+
|
44
|
+
# NOTE: You must run `pacman-key --init` before first using pacman; the local
|
45
|
+
# keyring can then be populated with the keys of all official Arch Linux
|
46
|
+
# packagers with `pacman-key --populate archlinux`.
|
47
|
+
|
48
|
+
#
|
49
|
+
# REPOSITORIES
|
50
|
+
# - can be defined here or included from another file
|
51
|
+
# - pacman will search repositories in the order defined here
|
52
|
+
# - local/custom mirrors can be added here or in separate files
|
53
|
+
# - repositories listed first will take precedence when packages
|
54
|
+
# have identical names, regardless of version number
|
55
|
+
# - URLs will have $repo replaced by the name of the current repo
|
56
|
+
# - URLs will have $arch replaced by the name of the architecture
|
57
|
+
#
|
58
|
+
# Repository entries are of the format:
|
59
|
+
# [repo-name]
|
60
|
+
# Server = ServerName
|
61
|
+
# Include = IncludePath
|
62
|
+
#
|
63
|
+
# The header [repo-name] is crucial - it must be present and
|
64
|
+
# uncommented to enable the repo.
|
65
|
+
#
|
66
|
+
|
67
|
+
# The testing repositories are disabled by default. To enable, uncomment the
|
68
|
+
# repo name header and Include lines. You can add preferred servers immediately
|
69
|
+
# after the header, and they will be used before the default mirrors.
|
70
|
+
|
71
|
+
#[testing]
|
72
|
+
#Include = /etc/pacman.d/mirrorlist
|
73
|
+
|
74
|
+
[core]
|
75
|
+
Include = /etc/pacman.d/mirrorlist
|
76
|
+
|
77
|
+
[extra]
|
78
|
+
Include = /etc/pacman.d/mirrorlist
|
79
|
+
|
80
|
+
#[community-testing]
|
81
|
+
#Include = /etc/pacman.d/mirrorlist
|
82
|
+
|
83
|
+
[community]
|
84
|
+
Include = /etc/pacman.d/mirrorlist
|
85
|
+
|
86
|
+
# If you want to run 32 bit applications on your x86_64 system,
|
87
|
+
# enable the multilib repositories as required here.
|
88
|
+
|
89
|
+
#[multilib-testing]
|
90
|
+
#Include = /etc/pacman.d/mirrorlist
|
91
|
+
|
92
|
+
#[multilib]
|
93
|
+
#Include = /etc/pacman.d/mirrorlist
|
94
|
+
|
95
|
+
# An example of a custom package repository. See the pacman manpage for
|
96
|
+
# tips on creating your own repositories.
|
97
|
+
#[custom]
|
98
|
+
#SigLevel = Optional TrustAll
|
99
|
+
#Server = file:///home/custompkgs
|
data/lib/akabei/repository.rb
CHANGED
@@ -9,9 +9,10 @@ module Akabei
|
|
9
9
|
class Repository
|
10
10
|
attr_accessor :signer, :include_files
|
11
11
|
|
12
|
-
def initialize
|
12
|
+
def initialize(opts = {})
|
13
13
|
@db = {}
|
14
|
-
@include_files = false
|
14
|
+
@include_files = opts[:include_files] || false
|
15
|
+
@signer = opts[:signer]
|
15
16
|
end
|
16
17
|
|
17
18
|
extend Forwardable
|
@@ -55,8 +56,8 @@ module Akabei
|
|
55
56
|
nil
|
56
57
|
end
|
57
58
|
|
58
|
-
def self.load(path)
|
59
|
-
new.tap do |repo|
|
59
|
+
def self.load(path, opts = {})
|
60
|
+
new(opts).tap do |repo|
|
60
61
|
repo.load(path)
|
61
62
|
end
|
62
63
|
end
|
data/lib/akabei/signer.rb
CHANGED
@@ -36,9 +36,13 @@ module Akabei
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
-
def
|
39
|
+
def self.get(gpg_key, crypto = nil)
|
40
|
+
gpg_key && new(gpg_key, crypto)
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(gpg_key, crypto = nil)
|
40
44
|
@gpg_key = find_secret_key(gpg_key)
|
41
|
-
@crypto = crypto
|
45
|
+
@crypto = crypto || GPGME::Crypto.new
|
42
46
|
end
|
43
47
|
|
44
48
|
def detach_sign(path)
|
data/lib/akabei/version.rb
CHANGED
data/spec/akabei/builder_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'akabei/builder'
|
|
3
3
|
require 'akabei/signer'
|
4
4
|
|
5
5
|
describe Akabei::Builder do
|
6
|
-
let(:builder) { described_class.new }
|
6
|
+
let(:builder) { described_class.new(pkgdest: pkgdest) }
|
7
7
|
let(:pkgdest) { test_dest('packages').tap(&:mkpath) }
|
8
8
|
let(:package_dir) { test_dest('nkf').tap(&:mkpath) }
|
9
9
|
let(:pkgname) { 'nkf-2.1.3-1' }
|
@@ -14,10 +14,6 @@ describe Akabei::Builder do
|
|
14
14
|
let(:log_package_fname) { "#{pkgname}-#{arch}-package.log" }
|
15
15
|
let(:srcpkg_fname) { "#{pkgname}.src.tar.gz" }
|
16
16
|
|
17
|
-
before do
|
18
|
-
builder.pkgdest = pkgdest
|
19
|
-
end
|
20
|
-
|
21
17
|
describe '#build_package' do
|
22
18
|
let(:chroot) { double('ChrootTree') }
|
23
19
|
|
data/spec/akabei/cli_spec.rb
CHANGED
@@ -2,4 +2,139 @@ require 'spec_helper'
|
|
2
2
|
require 'akabei/cli'
|
3
3
|
|
4
4
|
describe Akabei::CLI do
|
5
|
+
let(:cli) { described_class.new }
|
6
|
+
let(:package_dir) { test_dest('nkf') }
|
7
|
+
let(:srcpkg_path) { test_input('nkf.tar.gz') }
|
8
|
+
let(:package_path) { test_input('nkf-2.1.3-1-x86_64.pkg.tar.xz') }
|
9
|
+
|
10
|
+
describe '#build' do
|
11
|
+
let(:repo_dir) { test_dest('repo').tap(&:mkpath) }
|
12
|
+
let(:base_opts) { { arch: 'x86_64', repo_dir: repo_dir.to_s, repo_name: 'test' } }
|
13
|
+
let(:package) { double('built package') }
|
14
|
+
let(:entry) { Akabei::PackageEntry.new }
|
15
|
+
let(:chroot_expectations) { lambda { |chroot| } }
|
16
|
+
|
17
|
+
before do
|
18
|
+
tar('xf', test_input('nkf.tar.gz').to_s, '-C', package_dir.parent.to_s)
|
19
|
+
# Disable warning
|
20
|
+
entry.add('files', 'usr/bin/nkf')
|
21
|
+
|
22
|
+
allow(package).to receive(:db_name).and_return('nkf-2.1.3-1')
|
23
|
+
allow(package).to receive(:to_entry).and_return(entry)
|
24
|
+
|
25
|
+
allow_any_instance_of(Akabei::ChrootTree).to receive(:with_chroot) { |chroot, &block|
|
26
|
+
chroot_expectations.call(chroot)
|
27
|
+
block.call
|
28
|
+
}
|
29
|
+
allow_any_instance_of(Akabei::Builder).to receive(:build_package) { |builder, dir, chroot|
|
30
|
+
expect(builder).to receive(:with_source_package).with(package_dir.to_s).and_yield(srcpkg_path)
|
31
|
+
[package]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'calls Builder#build_package in chroot and create repository database' do
|
36
|
+
cli.invoke(:build, [package_dir.to_s], base_opts)
|
37
|
+
expect(repo_dir.join('test.db')).to be_file
|
38
|
+
expect(repo_dir.join('test.files')).to be_file
|
39
|
+
expect(repo_dir.join('test.abs.tar.gz')).to be_file
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with --makepkg-config' do
|
43
|
+
let(:makepkg_path) { test_input('makepkg.x86_64.conf') }
|
44
|
+
let(:chroot_expectations) {
|
45
|
+
lambda do |chroot|
|
46
|
+
expect(chroot.makepkg_config).to eq(makepkg_path)
|
47
|
+
end
|
48
|
+
}
|
49
|
+
|
50
|
+
it 'calls ChrootTree#with_chroot with makepkg_config' do
|
51
|
+
cli.invoke(:build, [package_dir.to_s], base_opts.merge(makepkg_config: makepkg_path.to_s))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with --pacman-config' do
|
56
|
+
let(:pacman_path) { test_input('pacman.x86_64.conf') }
|
57
|
+
let(:chroot_expectations) {
|
58
|
+
lambda do |chroot|
|
59
|
+
expect(chroot.pacman_config).to eq(pacman_path)
|
60
|
+
end
|
61
|
+
}
|
62
|
+
|
63
|
+
it 'calls ChrootTree#with_chroot with pacman_config' do
|
64
|
+
cli.invoke(:build, [package_dir.to_s], base_opts.merge(pacman_config: pacman_path.to_s))
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#abs_add' do
|
70
|
+
let(:repo_name) { 'test' }
|
71
|
+
let(:abs_path) { test_dest('abs.tar.gz') }
|
72
|
+
|
73
|
+
it 'creates abs tarball' do
|
74
|
+
allow_any_instance_of(Akabei::Builder).to receive(:with_source_package).with(package_dir.to_s).and_yield(srcpkg_path)
|
75
|
+
cli.invoke(:abs_add, [package_dir.to_s, abs_path.to_s], repo_name: repo_name)
|
76
|
+
expect(abs_path).to be_file
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#abs_remove' do
|
81
|
+
let(:repo_name) { 'test' }
|
82
|
+
let(:abs_path) { test_dest('abs.tar.gz') }
|
83
|
+
|
84
|
+
before do
|
85
|
+
FileUtils.cp(test_input('abs.tar.gz'), abs_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'removes package from abs tarball' do
|
89
|
+
cli.invoke(:abs_remove, ['htop-vi', abs_path.to_s], repo_name: repo_name)
|
90
|
+
expect(abs_path).to be_file
|
91
|
+
expect(tar('tf', abs_path.to_s)).to_not include('test/htop-vi/PKGBUILD')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#repo_add' do
|
96
|
+
let(:db_path) { test_dest('test.db') }
|
97
|
+
|
98
|
+
it 'creates repository database' do
|
99
|
+
cli.invoke(:repo_add, [package_path.to_s, db_path.to_s])
|
100
|
+
expect(db_path).to be_file
|
101
|
+
expect(tar('tf', db_path.to_s)).to include('nkf-2.1.3-1/desc')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#repo_remove' do
|
106
|
+
let(:db_path) { test_dest('test.db') }
|
107
|
+
|
108
|
+
before do
|
109
|
+
FileUtils.cp(test_input('test.db'), db_path)
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'removes package from repository database' do
|
113
|
+
cli.invoke(:repo_remove, ['htop-vi', db_path.to_s])
|
114
|
+
expect(tar('tf', db_path.to_s)).to_not include('htop-vi-1.0.2-4/desc')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#files_add' do
|
119
|
+
let(:files_path) { test_dest('test.files') }
|
120
|
+
|
121
|
+
it 'creates files database' do
|
122
|
+
cli.invoke(:files_add, [package_path.to_s, files_path.to_s])
|
123
|
+
expect(files_path).to be_file
|
124
|
+
expect(tar('tf', files_path.to_s)).to include('nkf-2.1.3-1/files')
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe '#files_remove' do
|
129
|
+
let(:files_path) { test_dest('test.files') }
|
130
|
+
|
131
|
+
before do
|
132
|
+
FileUtils.cp(test_input('test.files'), files_path)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'removes package from files database' do
|
136
|
+
cli.invoke(:files_remove, ['htop-vi', files_path.to_s])
|
137
|
+
expect(tar('tf', files_path.to_s)).to_not include('htop-vi-1.0.2-4/files')
|
138
|
+
end
|
139
|
+
end
|
5
140
|
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'akabei/cli'
|
3
|
+
|
4
|
+
class TestShell < Thor::Shell::Basic
|
5
|
+
attr_reader :stdout
|
6
|
+
|
7
|
+
def initialize(stdout)
|
8
|
+
@stdout = stdout
|
9
|
+
super()
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Akabei::Omakase::CLI do
|
14
|
+
let(:stdout) { StringIO.new }
|
15
|
+
let(:cli) { described_class.new }
|
16
|
+
|
17
|
+
before do
|
18
|
+
cli.shell = TestShell.new(stdout)
|
19
|
+
end
|
20
|
+
|
21
|
+
around do |example|
|
22
|
+
Dir.mktmpdir do |dir|
|
23
|
+
cli.inside(dir) do
|
24
|
+
Dir.chdir(dir) do
|
25
|
+
example.run
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#init' do
|
32
|
+
it 'creates template directories' do
|
33
|
+
cli.invoke(:init, ['test'])
|
34
|
+
here = Pathname.new('.')
|
35
|
+
expect(here.join('.akabei.yml')).to be_file
|
36
|
+
expect(here.join('test')).to be_directory
|
37
|
+
expect(here.join('sources')).to be_directory
|
38
|
+
expect(here.join('logs')).to be_directory
|
39
|
+
expect(here.join('PKGBUILDs')).to be_directory
|
40
|
+
%w[i686 x86_64].each do |arch|
|
41
|
+
%w[makepkg pacman].each do |conf|
|
42
|
+
expect(here.join('etc', "#{conf}.#{arch}.conf")).to be_file
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'creates valid config' do
|
48
|
+
cli.invoke(:init, ['test'])
|
49
|
+
config = Akabei::Omakase::Config.load
|
50
|
+
expect { config.validate! }.to_not raise_error
|
51
|
+
expect(config.name).to eq('test')
|
52
|
+
expect(config.srcdest).to be_directory
|
53
|
+
expect(config.logdest).to be_directory
|
54
|
+
expect(config.pkgbuild).to be_directory
|
55
|
+
config.builds.each do |arch, config_file|
|
56
|
+
expect(Pathname.new(config_file['makepkg'])).to be_file
|
57
|
+
expect(Pathname.new(config_file['pacman'])).to be_file
|
58
|
+
end
|
59
|
+
expect(config['s3']).to be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with --s3' do
|
63
|
+
it 'creates config with s3' do
|
64
|
+
cli.invoke(:init, ['test'], s3: true)
|
65
|
+
config = Akabei::Omakase::Config.load
|
66
|
+
expect { config.validate! }.to_not raise_error
|
67
|
+
expect(config['s3']).to_not be_nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#build' do
|
73
|
+
let(:config) { Akabei::Omakase::Config.load }
|
74
|
+
let(:packages) { { 'i686' => double('built package (i686)'), 'x86_64' => double('built package (x86_64)') } }
|
75
|
+
let(:entry) { Akabei::PackageEntry.new }
|
76
|
+
let(:init_opts) { {} }
|
77
|
+
|
78
|
+
before do
|
79
|
+
cli.invoke(:init, ['test'], init_opts)
|
80
|
+
tar('xf', test_input('nkf.tar.gz').to_s, '-C', config.pkgbuild.to_s)
|
81
|
+
|
82
|
+
packages.each do |arch, package|
|
83
|
+
allow(package).to receive(:db_name).and_return('nkf-2.1.3-1')
|
84
|
+
allow(package).to receive(:to_entry).and_return(entry)
|
85
|
+
allow(package).to receive(:path).and_return(Pathname.new("test/os/#{arch}/nkf-2.1.3-1-#{arch}.pkg.tar.xz"))
|
86
|
+
end
|
87
|
+
entry.add('files', 'usr/bin/nkf')
|
88
|
+
|
89
|
+
allow_any_instance_of(Akabei::ChrootTree).to receive(:with_chroot) { |chroot, &block|
|
90
|
+
expect(chroot.makepkg_config.to_s).to eq("etc/makepkg.#{chroot.arch}.conf")
|
91
|
+
expect(chroot.pacman_config.to_s).to eq("etc/pacman.#{chroot.arch}.conf")
|
92
|
+
block.call
|
93
|
+
}
|
94
|
+
expect(config['builds'].size).to eq(2)
|
95
|
+
|
96
|
+
allow_any_instance_of(Akabei::Builder).to receive(:build_package) { |builder, dir, chroot|
|
97
|
+
expect(builder).to receive(:with_source_package).with(config.package_dir('nkf')).and_yield(test_input('nkf.tar.gz'))
|
98
|
+
[packages[chroot.arch]]
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'builds a package and add it to repository' do
|
103
|
+
cli.invoke(:build, ['nkf'])
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'with --s3' do
|
107
|
+
let(:init_opts) { { s3: true } }
|
108
|
+
let(:access_key_id) { 'ACCESS/KEY' }
|
109
|
+
let(:secret_access_key) { 'SECRET/ACCESS/KEY' }
|
110
|
+
let(:bucket_name) { 'test.bucket.name' }
|
111
|
+
let(:region) { 'ap-northeast-1' }
|
112
|
+
|
113
|
+
let(:buckets) { double('S3::BucketCollection') }
|
114
|
+
let(:bucket) { double('S3::Bucket') }
|
115
|
+
let(:objects) { double('S3::ObjectCollection') }
|
116
|
+
let(:write_options) { { reduced_redundancy: true } }
|
117
|
+
|
118
|
+
before do
|
119
|
+
c = SafeYAML.load_file('.akabei.yml')
|
120
|
+
c['s3']['access_key_id'] = access_key_id
|
121
|
+
c['s3']['secret_access_key'] = secret_access_key
|
122
|
+
c['s3']['bucket'] = bucket_name
|
123
|
+
c['s3']['region'] = region
|
124
|
+
c['s3']['write_options'] = write_options
|
125
|
+
open('.akabei.yml', 'w') { |f| YAML.dump(c, f) }
|
126
|
+
|
127
|
+
allow_any_instance_of(AWS::S3).to receive(:buckets).and_return(buckets)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'uploads built packages and update repositories' do
|
131
|
+
expect(buckets).to receive(:[]).with(bucket_name).and_return(bucket)
|
132
|
+
allow(bucket).to receive(:objects).and_return(objects)
|
133
|
+
|
134
|
+
%w[i686 x86_64].each do |arch|
|
135
|
+
%w[test.db test.files test.abs.tar.gz].each do |fname|
|
136
|
+
obj = double("S3::Object #{fname}")
|
137
|
+
# download and upload
|
138
|
+
expect(objects).to receive(:[]).with("test/os/#{arch}/#{fname}").twice.and_return(obj)
|
139
|
+
expect(obj).to receive(:read).and_yield('')
|
140
|
+
expect(obj).to receive(:write)
|
141
|
+
end
|
142
|
+
|
143
|
+
# upload only
|
144
|
+
pkg = double("S3::Object built package (#{arch})")
|
145
|
+
db_name = "nkf-2.1.3-1-#{arch}.pkg.tar.xz"
|
146
|
+
expect(objects).to receive(:[]).with("test/os/#{arch}/#{db_name}").and_return(pkg)
|
147
|
+
expect(pkg).to receive(:write).with(anything, hash_including(write_options))
|
148
|
+
end
|
149
|
+
|
150
|
+
cli.invoke(:build, ['nkf'])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|