duck-installer 0.2.1
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.
- data/LICENSE +674 -0
- data/README +35 -0
- data/bin/duck +9 -0
- data/duck.yaml +93 -0
- data/files/etc/dhclient-exit-hooks.d/hostname +17 -0
- data/files/etc/fstab +1 -0
- data/files/etc/hostname +1 -0
- data/files/etc/hosts +2 -0
- data/files/etc/inittab +25 -0
- data/files/etc/mtab +1 -0
- data/files/init +9 -0
- data/files/lib/duck.d/00-splash +36 -0
- data/files/lib/duck.d/02-setup-network +30 -0
- data/files/lib/duck.d/40-debootstrap +16 -0
- data/files/lib/duck.d/41-add-policy-rc.d +14 -0
- data/files/lib/duck.d/41-update-hostname +9 -0
- data/files/lib/duck.d/98-remove-policy-rc.d +9 -0
- data/files/lib/duck.d/99-reboot +18 -0
- data/files/lib/libduck.sh +152 -0
- data/files/lib/python-duck/duck/__init__.py +2 -0
- data/files/lib/python-duck/duck/db.py +48 -0
- data/files/lib/python-duck/duck/log.py +5 -0
- data/files/sbin/duckdb +212 -0
- data/files/sbin/duckinstall +21 -0
- data/files/sbin/ducklogin +3 -0
- data/fixes/clear-persistent-udev +10 -0
- data/fixes/kernel-boot-fix +53 -0
- data/fixes/squeeze-fix +14 -0
- data/lib/duck.rb +191 -0
- data/lib/duck/build.rb +395 -0
- data/lib/duck/chroot_utils.rb +51 -0
- data/lib/duck/enter.rb +20 -0
- data/lib/duck/logging.rb +31 -0
- data/lib/duck/module_helper.rb +56 -0
- data/lib/duck/pack.rb +42 -0
- data/lib/duck/qemu.rb +34 -0
- data/lib/duck/spawn_utils.rb +83 -0
- data/lib/duck/version.rb +3 -0
- metadata +103 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
import dbm
|
2
|
+
import json
|
3
|
+
import contextlib
|
4
|
+
|
5
|
+
DEFAULT_DB = "/var/duck"
|
6
|
+
DEFAULT_ENCODING = 'utf-8'
|
7
|
+
|
8
|
+
|
9
|
+
class DBSession(object):
|
10
|
+
def __init__(self, db, encoding):
|
11
|
+
self._db = db
|
12
|
+
self._encoding = encoding
|
13
|
+
|
14
|
+
def get(self, key, default=None):
|
15
|
+
try:
|
16
|
+
value = self._db[key]
|
17
|
+
except KeyError:
|
18
|
+
return default
|
19
|
+
|
20
|
+
if value is None:
|
21
|
+
return None
|
22
|
+
|
23
|
+
value = value.decode(self._encoding)
|
24
|
+
value = json.loads(value)
|
25
|
+
return value
|
26
|
+
|
27
|
+
def set(self, key, value):
|
28
|
+
value = json.dumps(value)
|
29
|
+
value = value.encode(self._encoding)
|
30
|
+
self._db[key] = value
|
31
|
+
|
32
|
+
def keys(self):
|
33
|
+
return self._db.keys()
|
34
|
+
|
35
|
+
|
36
|
+
class DB(object):
|
37
|
+
def __init__(self, path=None, encoding=None):
|
38
|
+
if path is None:
|
39
|
+
path = DEFAULT_DB
|
40
|
+
if encoding is None:
|
41
|
+
encoding = DEFAULT_ENCODING
|
42
|
+
self._full_path = "{0}.dbm".format(path)
|
43
|
+
self._encoding = encoding
|
44
|
+
|
45
|
+
@contextlib.contextmanager
|
46
|
+
def open(self):
|
47
|
+
with contextlib.closing(dbm.open(self._full_path, "c")) as db:
|
48
|
+
yield DBSession(db, self._encoding)
|
data/files/sbin/duckdb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
#!/usr/bin/python
|
2
|
+
#
|
3
|
+
# AutoDB is used to store duckinstaller variables.
|
4
|
+
#
|
5
|
+
|
6
|
+
import duck.db as duck_db
|
7
|
+
import duck
|
8
|
+
|
9
|
+
import argparse
|
10
|
+
import urllib2
|
11
|
+
import json
|
12
|
+
import contextlib
|
13
|
+
|
14
|
+
VERSION = duck.__version_string__
|
15
|
+
|
16
|
+
|
17
|
+
def update_db(function):
|
18
|
+
def inner(ns):
|
19
|
+
with ns.db.open() as S:
|
20
|
+
for key, value in function(S, ns):
|
21
|
+
if ns.ns:
|
22
|
+
key = "%s/%s" % (ns.ns, key)
|
23
|
+
S.set(key, value)
|
24
|
+
|
25
|
+
return 0
|
26
|
+
|
27
|
+
return inner
|
28
|
+
|
29
|
+
|
30
|
+
def parse_text(fd):
|
31
|
+
for line in fd:
|
32
|
+
line = line.strip()
|
33
|
+
|
34
|
+
if line.startswith("#") or not line:
|
35
|
+
continue
|
36
|
+
|
37
|
+
key, value = line.split(" ", 2)
|
38
|
+
yield key, value
|
39
|
+
|
40
|
+
|
41
|
+
def flatten_dict(doc, keys=[]):
|
42
|
+
result = dict()
|
43
|
+
|
44
|
+
for key, value in doc.items():
|
45
|
+
this_keys = keys + [key]
|
46
|
+
|
47
|
+
if isinstance(value, dict):
|
48
|
+
result.update(flatten_dict(value, this_keys))
|
49
|
+
else:
|
50
|
+
result["/".join(this_keys)] = value
|
51
|
+
|
52
|
+
return result
|
53
|
+
|
54
|
+
|
55
|
+
def parse_json(fd):
|
56
|
+
doc = json.load(fd)
|
57
|
+
|
58
|
+
for key, value in doc.items():
|
59
|
+
yield key, value
|
60
|
+
|
61
|
+
|
62
|
+
def parse_cmdline(fd):
|
63
|
+
cmdline = fd.read()
|
64
|
+
|
65
|
+
for c in cmdline.split(" "):
|
66
|
+
c = c.strip()
|
67
|
+
|
68
|
+
if not c or '=' not in c:
|
69
|
+
continue
|
70
|
+
|
71
|
+
key, value = c.split("=", 2)
|
72
|
+
yield key, value
|
73
|
+
|
74
|
+
|
75
|
+
def read_dict(mode, fd):
|
76
|
+
if mode == 'cmdline':
|
77
|
+
return parse_cmdline(fd)
|
78
|
+
|
79
|
+
if mode == 'json':
|
80
|
+
return parse_json(fd)
|
81
|
+
|
82
|
+
if mode == 'text':
|
83
|
+
return parse_text(fd)
|
84
|
+
|
85
|
+
raise Exception("Unknown file mode: {0}".format(mode))
|
86
|
+
|
87
|
+
|
88
|
+
def read(ns, fd):
|
89
|
+
value = dict(read_dict(ns.mode, fd))
|
90
|
+
|
91
|
+
if ns.flatten:
|
92
|
+
return flatten_dict(value)
|
93
|
+
|
94
|
+
return value
|
95
|
+
|
96
|
+
|
97
|
+
def action_get(ns):
|
98
|
+
with ns.db.open() as S:
|
99
|
+
value = S.get(ns.key, ns.default)
|
100
|
+
|
101
|
+
if value is None:
|
102
|
+
ok = False
|
103
|
+
value = ""
|
104
|
+
else:
|
105
|
+
ok = True
|
106
|
+
|
107
|
+
if ns.sh:
|
108
|
+
print "DUCK_RETURN=\'%s\';" % (value,)
|
109
|
+
print "DUCK_OK=\"%s\"" % ("yes" if ok else "no",)
|
110
|
+
return 0
|
111
|
+
|
112
|
+
if not ok:
|
113
|
+
return 1
|
114
|
+
|
115
|
+
if ns.raw:
|
116
|
+
value = repr(value)
|
117
|
+
|
118
|
+
print value
|
119
|
+
|
120
|
+
return 0
|
121
|
+
|
122
|
+
|
123
|
+
@update_db
|
124
|
+
def action_set(S, ns):
|
125
|
+
if ns.json:
|
126
|
+
value = json.loads(ns.value)
|
127
|
+
elif ns.value == '-':
|
128
|
+
value = sys.stdin.read()
|
129
|
+
else:
|
130
|
+
value = ns.value
|
131
|
+
|
132
|
+
yield ns.key, value
|
133
|
+
|
134
|
+
|
135
|
+
@update_db
|
136
|
+
def action_url(S, ns):
|
137
|
+
with contextlib.closing(urllib2.urlopen(ns.url)) as fd:
|
138
|
+
for key, value in read(ns, fd).items():
|
139
|
+
yield key, value
|
140
|
+
|
141
|
+
|
142
|
+
def action_list(ns):
|
143
|
+
with ns.db.open() as S:
|
144
|
+
for key in S.keys():
|
145
|
+
print key, repr(S.get(key))
|
146
|
+
|
147
|
+
|
148
|
+
def main(args):
|
149
|
+
parser = argparse.ArgumentParser(
|
150
|
+
usage="usage: %(prog)s [options] <action> [action-options]")
|
151
|
+
|
152
|
+
parser.add_argument("-v", "--version", action='version', version=VERSION)
|
153
|
+
|
154
|
+
parser.add_argument(
|
155
|
+
"--db",
|
156
|
+
metavar="<path>",
|
157
|
+
default=None)
|
158
|
+
|
159
|
+
parser.add_argument(
|
160
|
+
"--ns",
|
161
|
+
metavar="<namespace>",
|
162
|
+
default=None)
|
163
|
+
|
164
|
+
parsers = parser.add_subparsers()
|
165
|
+
|
166
|
+
get_parser = parsers.add_parser("get", help="Get a value")
|
167
|
+
get_parser.add_argument("key")
|
168
|
+
get_parser.add_argument("default", nargs='?', default=None)
|
169
|
+
get_parser.add_argument("--sh",
|
170
|
+
help=("Output return value as a shell "
|
171
|
+
"evaluable string"),
|
172
|
+
default=False,
|
173
|
+
action='store_true')
|
174
|
+
get_parser.add_argument("--raw",
|
175
|
+
help=("Output raw (repr) value"),
|
176
|
+
default=False,
|
177
|
+
action='store_true')
|
178
|
+
get_parser.set_defaults(action=action_get)
|
179
|
+
|
180
|
+
set_parser = parsers.add_parser("set", help="Set a value")
|
181
|
+
set_parser.add_argument("--json",
|
182
|
+
help="treat argument as json",
|
183
|
+
default=False,
|
184
|
+
action='store_true')
|
185
|
+
set_parser.add_argument("key")
|
186
|
+
set_parser.add_argument("value")
|
187
|
+
set_parser.set_defaults(action=action_set)
|
188
|
+
|
189
|
+
list_parser = parsers.add_parser("list", help="List all values")
|
190
|
+
list_parser.set_defaults(action=action_list)
|
191
|
+
|
192
|
+
url_parser = parsers.add_parser("url",
|
193
|
+
help="Read values from an url")
|
194
|
+
url_parser.add_argument("url", help="Fetch values from specified url")
|
195
|
+
url_parser.add_argument("--json", dest='mode',
|
196
|
+
help="Treat input as a json document",
|
197
|
+
action='store_const', const='json')
|
198
|
+
url_parser.add_argument("--cmdline", dest='mode',
|
199
|
+
help="Treat input as a /proc/cmdline file",
|
200
|
+
action='store_const', const='cmdline')
|
201
|
+
url_parser.add_argument("--flatten",
|
202
|
+
help="Flatten a nested dictionary",
|
203
|
+
default=False, action='store_true')
|
204
|
+
url_parser.set_defaults(action=action_url, mode='file')
|
205
|
+
|
206
|
+
ns = parser.parse_args(args)
|
207
|
+
ns.db = duck_db.DB(path=ns.db)
|
208
|
+
return ns.action(ns)
|
209
|
+
|
210
|
+
if __name__ == "__main__":
|
211
|
+
import sys
|
212
|
+
sys.exit(main(sys.argv[1:]))
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
. /lib/libduck.sh
|
3
|
+
a_get_into log duck/log "$DEFAULT_LOG"
|
4
|
+
a_get_into error_command 'duck/error-command' "$DUCK_LOGIN"
|
5
|
+
a_get_into success_command 'duck/success-command' "$DUCK_LOGIN"
|
6
|
+
|
7
|
+
info "Duck Installer $DUCK_VERSION"
|
8
|
+
|
9
|
+
if [[ -f $INSTALLER_STATUS ]]; then
|
10
|
+
info "Not running installation, $INSTALLER_STATUS exists"
|
11
|
+
exec $DUCK_LOGIN
|
12
|
+
fi
|
13
|
+
|
14
|
+
info "Running installation, logging to $log and syslog"
|
15
|
+
|
16
|
+
if ! run_installer 2>&1 | tee -a $log | logger -t duckinstall -s; then
|
17
|
+
exec $error_command
|
18
|
+
fi
|
19
|
+
|
20
|
+
touch $INSTALLER_STATUS
|
21
|
+
exec $success_command
|
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Remove any persistent udev rules
|
3
|
+
# If the host system runs it's own udev daemon, there is a chance that installation
|
4
|
+
# hooks will trigger the generation of persistent files.
|
5
|
+
|
6
|
+
set -e
|
7
|
+
|
8
|
+
case "$1" in
|
9
|
+
"final") rm -f /etc/udev/rules.d/*; ;;
|
10
|
+
esac
|
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
# Make sure that the kernel does not generate an initramfs.
|
3
|
+
# Clear /boot before packages are being configured.
|
4
|
+
|
5
|
+
set -e
|
6
|
+
|
7
|
+
status_code=0
|
8
|
+
|
9
|
+
case "$1" in
|
10
|
+
"pre-packages-configure")
|
11
|
+
echo "Disabling generation of an initramfs"
|
12
|
+
|
13
|
+
rm -rf /etc/kernel/postinst.d
|
14
|
+
rm -rf /etc/kernel/postrm.d
|
15
|
+
|
16
|
+
dpkg --get-selections | while read name _; do
|
17
|
+
case "$name" in
|
18
|
+
linux-image-*-dbg) continue ;;
|
19
|
+
linux-image-*) ;;
|
20
|
+
*) continue ;;
|
21
|
+
esac
|
22
|
+
|
23
|
+
version=${name#linux-image-*}
|
24
|
+
|
25
|
+
path="/var/lib/dpkg/info/$name.postinst"
|
26
|
+
|
27
|
+
[[ ! -f $path ]] && continue
|
28
|
+
|
29
|
+
if ! grep -E 'my \$initrd\s+=\s+"YES";' $path; then
|
30
|
+
echo "initrd already disabled: $name"
|
31
|
+
continue
|
32
|
+
fi
|
33
|
+
|
34
|
+
cp -a $path $path.original
|
35
|
+
|
36
|
+
echo "Disabling initrd for: $name"
|
37
|
+
|
38
|
+
if ! sed -r -i 's/my \$initrd\s+=\s+"YES";/my $initrd = "";/' $path; then
|
39
|
+
echo "failed to patch: $path"
|
40
|
+
exit 1
|
41
|
+
fi
|
42
|
+
|
43
|
+
echo "Generating modules.dep"
|
44
|
+
depmod $version
|
45
|
+
done
|
46
|
+
;;
|
47
|
+
"final")
|
48
|
+
echo "Clearing /boot"
|
49
|
+
rm -rf /boot
|
50
|
+
;;
|
51
|
+
esac
|
52
|
+
|
53
|
+
exit 0
|
data/fixes/squeeze-fix
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# fix for dash hooks that does not run properly on it's own.
|
3
|
+
# http://wiki.debian.org/Multistrap#Steps_for_Squeeze_and_later
|
4
|
+
|
5
|
+
set -e
|
6
|
+
|
7
|
+
case "$1" in
|
8
|
+
"pre-bootstrap-configure")
|
9
|
+
mkdir -p /usr/share/man/man1
|
10
|
+
;;
|
11
|
+
*) ;;
|
12
|
+
esac
|
13
|
+
|
14
|
+
exit 0
|
data/lib/duck.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'yaml'
|
4
|
+
require 'find'
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
require 'duck/logging'
|
8
|
+
require 'duck/build'
|
9
|
+
require 'duck/enter'
|
10
|
+
require 'duck/pack'
|
11
|
+
require 'duck/qemu'
|
12
|
+
|
13
|
+
module Duck
|
14
|
+
class << self
|
15
|
+
include Logging
|
16
|
+
end
|
17
|
+
|
18
|
+
# environment to prevent tasks from being interactive.
|
19
|
+
DEFAULT_SHELL = '/bin/bash'
|
20
|
+
CONFIG_NAME = 'duck.yaml'
|
21
|
+
CONFIG_ARRAYS = [:files, :packages, :transports, :preferences, :fixes, :services, :sources]
|
22
|
+
|
23
|
+
ACTTIONS = {
|
24
|
+
:build => Duck::Build,
|
25
|
+
:enter => Duck::Enter,
|
26
|
+
:pack => Duck::Pack,
|
27
|
+
:qemu => Duck::Qemu,
|
28
|
+
}
|
29
|
+
|
30
|
+
def self.resource_path(path)
|
31
|
+
File.expand_path File.join('..', '..', path), __FILE__
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.parse_options(args)
|
35
|
+
o = Hash.new
|
36
|
+
|
37
|
+
working_directory = Dir.pwd
|
38
|
+
|
39
|
+
o[:temp] = File.join working_directory, 'tmp'
|
40
|
+
o[:target] = File.join o[:temp], 'initrd'
|
41
|
+
o[:initrd] = File.join o[:temp], 'initrd.gz'
|
42
|
+
o[:gpg_homedir] = File.join o[:temp], 'gpg'
|
43
|
+
o[:kernel] = File.join working_directory, 'vmlinuz'
|
44
|
+
o[:no_minimize] = false
|
45
|
+
o[:append] = nil
|
46
|
+
o[:keep_minimized] = false
|
47
|
+
o[:shell] = DEFAULT_SHELL
|
48
|
+
o[:_configs] = []
|
49
|
+
o[:_roots] = []
|
50
|
+
|
51
|
+
CONFIG_ARRAYS.each do |array|
|
52
|
+
o[array] = []
|
53
|
+
end
|
54
|
+
|
55
|
+
action_names = [:build, :pack]
|
56
|
+
|
57
|
+
opts = OptionParser.new do |opts|
|
58
|
+
opts.banner = 'Usage: duck [action] [options]'
|
59
|
+
|
60
|
+
opts.on('-t <dir>', '--target <dir>',
|
61
|
+
'Build in the specified target directory') do |dir|
|
62
|
+
o[:target] = dir
|
63
|
+
end
|
64
|
+
|
65
|
+
opts.on('--no-minimize',
|
66
|
+
'Do not minimize the installation right before packing') do |dir|
|
67
|
+
o[:no_minimize] = true
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on('--keep-minimized',
|
71
|
+
'Keep the minimized version of the initrd around') do |dir|
|
72
|
+
o[:keep_minimized] = true
|
73
|
+
end
|
74
|
+
|
75
|
+
opts.on('--debug',
|
76
|
+
'Switch on debug logging') do |dir|
|
77
|
+
Logging::set_level Logger::DEBUG
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on('-o <file>', '--output <file>',
|
81
|
+
'Output the resulting initrd in the specified path') do |path|
|
82
|
+
o[:initrd] = path
|
83
|
+
end
|
84
|
+
|
85
|
+
opts.on('-k <kernel>', '--kernel <kernel>',
|
86
|
+
'Specify kernel to use when running qemu') do |path|
|
87
|
+
o[:kernel] = path
|
88
|
+
end
|
89
|
+
|
90
|
+
opts.on('-a <append>', '--append <append>',
|
91
|
+
'Specify kernel options to append') do |append|
|
92
|
+
o[:append] = append
|
93
|
+
end
|
94
|
+
|
95
|
+
opts.on('-c <path>', '--config <path>',
|
96
|
+
'Use the specified configuration path') do |path|
|
97
|
+
o[:_configs] << path
|
98
|
+
end
|
99
|
+
|
100
|
+
opts.on('-s <shell>', '--shell <shell>',
|
101
|
+
'Set the shell to use when chrooting') do |shell|
|
102
|
+
o[:shell] = shell
|
103
|
+
end
|
104
|
+
|
105
|
+
opts.on('-h', '--help', 'Show this message') do
|
106
|
+
puts opts
|
107
|
+
return nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
args = opts.parse! args
|
112
|
+
|
113
|
+
unless args.empty?
|
114
|
+
action_names = args.map{|a| a.to_sym}
|
115
|
+
end
|
116
|
+
|
117
|
+
# add default configuration if none is specified.
|
118
|
+
if o[:_configs].empty?
|
119
|
+
o[:_configs] << File.join(working_directory, CONFIG_NAME)
|
120
|
+
end
|
121
|
+
|
122
|
+
o[:_configs] = [resource_path(CONFIG_NAME)] + o[:_configs]
|
123
|
+
|
124
|
+
o[:_configs].uniq!
|
125
|
+
o[:_configs].reject!{|i| not File.file? i}
|
126
|
+
return action_names, o
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.deep_symbolize(o)
|
130
|
+
return o.map{|i| deep_symbolize(i)} if o.is_a? Array
|
131
|
+
return o unless o.is_a? Hash
|
132
|
+
c = o.clone
|
133
|
+
c.keys.each {|k| c[k.to_sym] = deep_symbolize(c.delete(k))}
|
134
|
+
return c
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.prepare_options(o)
|
138
|
+
raise "No configuration found" if o[:_configs].empty?
|
139
|
+
|
140
|
+
[:target].each do |s|
|
141
|
+
next if File.directory? o[s]
|
142
|
+
log.info "Creating directory '#{s}' on #{o[s]}"
|
143
|
+
FileUtils.mkdir_p o[s]
|
144
|
+
end
|
145
|
+
|
146
|
+
unless File.directory? o[:gpg_homedir]
|
147
|
+
log.info "Creating directory GPG home directory on #{o[:gpg_homedir]}"
|
148
|
+
FileUtils.mkdir_p o[:gpg_homedir]
|
149
|
+
FileUtils.chmod 0700, o[:gpg_homedir]
|
150
|
+
end
|
151
|
+
|
152
|
+
o[:_configs].each do |config_path|
|
153
|
+
log.info "Loading configuration from #{config_path}"
|
154
|
+
config = deep_symbolize YAML.load_file(config_path)
|
155
|
+
root = File.dirname config_path
|
156
|
+
# Special keys treated as accumulated arrays over all configurations.
|
157
|
+
|
158
|
+
CONFIG_ARRAYS.each do |n|
|
159
|
+
o[n] += (config.delete(n) || []).map{|i| [root, i]}
|
160
|
+
end
|
161
|
+
|
162
|
+
# Merge (overwrite) the rest.
|
163
|
+
o.merge! config
|
164
|
+
o[:_roots] << root
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.main(args)
|
169
|
+
action_names, o = parse_options args
|
170
|
+
return 0 if o.nil?
|
171
|
+
prepare_options o
|
172
|
+
|
173
|
+
action_names.each do |action_name|
|
174
|
+
action_class = ACTTIONS[action_name]
|
175
|
+
|
176
|
+
if action_class.nil?
|
177
|
+
log.error "No such action: #{action_name}"
|
178
|
+
return 1
|
179
|
+
end
|
180
|
+
|
181
|
+
action_instance = action_class.new o
|
182
|
+
action_instance.execute
|
183
|
+
end
|
184
|
+
|
185
|
+
return 0
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
if __FILE__ == $0
|
190
|
+
exit Duck::main(ARGV)
|
191
|
+
end
|