dgd-tools 0.1.6 → 0.1.11
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 +4 -4
- data/Gemfile.lock +3 -1
- data/README.md +14 -0
- data/dgd-tools.gemspec +1 -0
- data/exe/dgd-manifest +72 -49
- data/exe/skotos-xml-diff +17 -17
- data/goods/chattheatre_kernellib.goods +12 -0
- data/lib/dgd-tools/manifest.rb +227 -68
- data/lib/dgd-tools/skotos_xml_obj.rb +129 -16
- data/lib/dgd-tools/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 838abb7957cb56eb3f096e8e345b3afda6d12db91a6110b7fa07029d589124d5
|
4
|
+
data.tar.gz: aa0a2551b3763eb418f99352cb0d40d58bcae92ff50bfce31bff22c644ae7019
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a661db27d13a97f9086c7802769e1a08ac3f21f7c120f8bea6a6efe6e332a3d2ecb7e2571e0020dd36c8188019af907023662e248c1c1ffadfd9ee3c1b9c5b2a
|
7
|
+
data.tar.gz: 15e3742398cb16222e176dc5bc9385445219e23fce1a0cdf9205ced86ad47570c4c422d637734386f6f79e154282c3f57cf02784f9e4238f1ec8208dc4944878
|
data/Gemfile.lock
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dgd-tools (0.1.
|
4
|
+
dgd-tools (0.1.10)
|
5
5
|
nokogiri (~> 1.10.5)
|
6
|
+
optimist (~> 3.0.1)
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: https://rubygems.org/
|
@@ -11,6 +12,7 @@ GEM
|
|
11
12
|
minitest (5.14.2)
|
12
13
|
nokogiri (1.10.10)
|
13
14
|
mini_portile2 (~> 2.4.0)
|
15
|
+
optimist (3.0.1)
|
14
16
|
rake (12.3.3)
|
15
17
|
|
16
18
|
PLATFORMS
|
data/README.md
CHANGED
@@ -14,6 +14,18 @@ DGD Manifest is a simple initial library system. I'm sure I'll figure more out a
|
|
14
14
|
|
15
15
|
This work has grown out of [SkotOS and ChatTheatre](https://github.com/ChatTheatre) tasks.
|
16
16
|
|
17
|
+
You can find example DGD manifest files under the "test" directory and also in [various](https://github.com/noahgibbs/prototype_vRWOT) [SkotOS-based games](https://github.com/ChatTheatre/gables_game) that use the DGD Manifest system.
|
18
|
+
|
19
|
+
You can find example "goods" (library) files under the "goods" subdirectory of this repo.
|
20
|
+
|
21
|
+
## WOE Objects and skotos-xml-diff
|
22
|
+
|
23
|
+
SkotOS-based games use an XML format for in-game objects called WOE, which is [documented in SkotOS-Doc](https://ChatTheatre.github.io/SkotOS-Doc). The skotos-xml-diff utility will diff between WOE objects or directories of WOE objects.
|
24
|
+
|
25
|
+
See SkotOS-Doc for more detail about how this can be used with a SkotOS game.
|
26
|
+
|
27
|
+
Run "skotos-xml-diff --help" for a list of options. You can tell it to ignore whitespace, to diff only the Merry (script) contents of the objects, and to ignore certain XML node types.
|
28
|
+
|
17
29
|
## Installation
|
18
30
|
|
19
31
|
You would normally install DGDTools directly: `gem install dgd-tools`.
|
@@ -26,6 +38,8 @@ If you have a DGD application that uses DGD Manifest, run `dgd-manifest install`
|
|
26
38
|
|
27
39
|
That fully-assembled DGD directory is named ".root". To run your dgd server, type "dgd-manifest server".
|
28
40
|
|
41
|
+
If you update files in your root and want to update files under the generated root directory, use "dgd-manifest update".
|
42
|
+
|
29
43
|
## Using DGD Manifest with your DGD Application
|
30
44
|
|
31
45
|
Your app will need a dgd.manifest file, which is a lot like NPM's package.json file.
|
data/dgd-tools.gemspec
CHANGED
data/exe/dgd-manifest
CHANGED
@@ -1,59 +1,82 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require "optimist"
|
3
4
|
require "dgd-tools/manifest"
|
4
5
|
|
5
|
-
|
6
|
-
ARGV.push "install"
|
7
|
-
end
|
6
|
+
SUB_COMMANDS = %w(new test install update server)
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
OPTS = Optimist::options do
|
9
|
+
version "DGD-tools version #{DGD::VERSION}"
|
10
|
+
banner <<BANNER
|
11
|
+
Use dgd.manifest to assemble your DGD application.
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
dgd
|
13
|
+
Available subcommands:
|
14
|
+
new [project_name]: create a new DGD-manifest project
|
15
|
+
test: make sure the dgd.manifest file is well-formed and usable
|
16
|
+
install: compile the DGD application to a config file and a root directory
|
17
|
+
update: copy files into generated root directory but do *not* clear 'extra' files (e.g. user data)
|
18
|
+
server: run DGD with the generated root and configuration
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
exit
|
20
|
+
Available options:
|
21
|
+
BANNER
|
22
|
+
opt :verbose, "Print verbose output where available"
|
23
|
+
stop_on SUB_COMMANDS
|
23
24
|
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
26
|
+
ARGV.push("install") if ARGV.size == 0
|
27
|
+
cmd = ARGV.shift
|
28
|
+
cmd_opts = case cmd
|
29
|
+
when "test"
|
30
|
+
#Optimist::options do
|
31
|
+
# opt :harsh, "check as exactly as possible"
|
32
|
+
#end
|
33
|
+
|
34
|
+
unless File.exist?("dgd.manifest")
|
35
|
+
raise "I don't see a dgd.manifest file in this directory!"
|
36
|
+
end
|
37
|
+
puts "Running dgd.manifest installer..."
|
38
|
+
repo = DGD::Manifest::Repo.new
|
39
|
+
repo.manifest_file("dgd.manifest")
|
40
|
+
repo.precheck(".", verbose: OPTS[:verbose])
|
41
|
+
puts "Verified Manifest packages: this looks likely correct."
|
42
|
+
|
43
|
+
when "install"
|
44
|
+
unless File.exist?("dgd.manifest")
|
45
|
+
raise "I don't see a dgd.manifest file in this directory!"
|
46
|
+
end
|
47
|
+
puts "Running DGD Manifest installer..."
|
48
|
+
repo = DGD::Manifest::Repo.new
|
49
|
+
repo.manifest_file("dgd.manifest")
|
50
|
+
current_dir = File.expand_path(".")
|
51
|
+
repo.precheck(current_dir, verbose: OPTS[:verbose])
|
52
|
+
repo.assemble_app(current_dir, verbose: OPTS[:verbose])
|
53
|
+
puts "Assembled DGD application into #{current_dir}"
|
54
|
+
|
55
|
+
when "update"
|
56
|
+
unless File.exist?("dgd.manifest")
|
57
|
+
raise "I don't see a dgd.manifest file in this directory!"
|
58
|
+
end
|
59
|
+
puts "Running DGD Manifest installer..."
|
60
|
+
repo = DGD::Manifest::Repo.new
|
61
|
+
repo.manifest_file("dgd.manifest")
|
62
|
+
current_dir = File.expand_path(".")
|
63
|
+
repo.precheck(current_dir, verbose: OPTS[:verbose])
|
64
|
+
repo.update_app(current_dir, verbose: OPTS[:verbose])
|
65
|
+
puts "Updated DGD application in #{current_dir}"
|
66
|
+
|
67
|
+
when "server"
|
68
|
+
puts "Starting DGD server..."
|
69
|
+
DGD::Manifest.system_call("~/.dgd-tools/dgd/bin/dgd dgd.config")
|
70
|
+
|
71
|
+
when "new"
|
72
|
+
unless ARGV.size == 1
|
73
|
+
puts "Usage: dgd-manifest new [project name]"
|
74
|
+
Optimist::die "Must supply exactly one argument to dgd-manifest new!"
|
75
|
+
end
|
76
|
+
appdir = DGD::Manifest::AppDirectory.new(File.expand_path ARGV[0])
|
77
|
+
appdir.name = ARGV[0]
|
78
|
+
appdir.create!
|
79
|
+
|
80
|
+
else
|
81
|
+
Optimist::die "Unknown subcommand: #{cmd.inspect}"
|
46
82
|
end
|
47
|
-
puts "Running DGD Manifest installer..."
|
48
|
-
repo = DGD::Manifest::Repo.new
|
49
|
-
repo.manifest_file("dgd.manifest")
|
50
|
-
current_dir = File.expand_path(".")
|
51
|
-
repo.precheck(current_dir)
|
52
|
-
repo.assemble_app(current_dir)
|
53
|
-
puts "Assembled DGD application into #{current_dir}"
|
54
|
-
when "server"
|
55
|
-
puts "Starting DGD server..."
|
56
|
-
DGD::Manifest.system_call("~/.dgd-tools/dgd/bin/dgd dgd.config")
|
57
|
-
else
|
58
|
-
raise "Unrecognised #{$0} command: #{ARGV[0].inspect}!"
|
59
|
-
end
|
data/exe/skotos-xml-diff
CHANGED
@@ -4,28 +4,28 @@
|
|
4
4
|
# SkotOS-or-similar game to find out what updates should
|
5
5
|
# be merged between them.
|
6
6
|
|
7
|
+
require 'optimist'
|
7
8
|
require "dgd-tools/skotos_xml_obj"
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
OPTS = Optimist::options do
|
11
|
+
version "DGD-tools version #{DGD::VERSION}"
|
12
|
+
banner <<BANNER
|
13
|
+
Usage:
|
14
|
+
skotos-xml-diff [options] <file_or_dir_1> <file_or_dir_2>
|
15
|
+
where [options] are:
|
16
|
+
BANNER
|
17
|
+
opt :version, "Print DGD version and exit"
|
18
|
+
opt :merry_only, "Only diff Merry scripts"
|
19
|
+
opt :ignore_whitespace, "Ignore whitespace in final diff"
|
20
|
+
opt :ignore_types, "Comma-separated list of XML node types to ignore (e.g. Combat:Base,Base:Exit)", type: :string
|
18
21
|
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
exit -1
|
23
|
-
end
|
23
|
+
Optimist::die "Supply exactly two files or directories -- you supplied #{ARGV.size}!" unless ARGV.size == 2
|
24
|
+
Optimist::die "Supply both files or both directories, not one of each!" if File.directory?(ARGV[0]) ^ File.directory?(ARGV[1])
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
26
|
+
SkotOS::XMLObject.merry_only = true if OPTS[:merry_only]
|
27
|
+
SkotOS::XMLObject.ignore_whitespace = true if OPTS[:ignore_whitespace]
|
28
|
+
SkotOS::XMLObject.ignore_types = OPTS[:ignore_types].split(",") if OPTS[:ignore_types]
|
29
29
|
|
30
30
|
if File.directory?(ARGV[0])
|
31
31
|
diff = SkotOS::XMLObject.diff_dirs(*ARGV)
|
data/lib/dgd-tools/manifest.rb
CHANGED
@@ -16,14 +16,13 @@ module DGD::Manifest
|
|
16
16
|
}
|
17
17
|
KERNEL_PATHS = KERNEL_PATH_MAP.values
|
18
18
|
DEFAULT_KERNELLIB_URL = "https://github.com/ChatTheatre/kernellib"
|
19
|
-
|
20
19
|
GENERATED_ROOT = ".root"
|
21
20
|
|
22
21
|
def self.system_call(cmd)
|
23
22
|
puts "Running command: #{cmd.inspect}..."
|
24
23
|
system(cmd, out: $stdout, err: :out)
|
25
24
|
unless $?.success?
|
26
|
-
raise "Error running command: #{cmd.inspect}!"
|
25
|
+
raise "Error running command in #{Dir.pwd}: #{cmd.inspect}!"
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
@@ -65,25 +64,26 @@ module DGD::Manifest
|
|
65
64
|
raise "Already have a dgd.manifest file!" unless @no_manifest_file
|
66
65
|
|
67
66
|
@no_manifest_file = false
|
68
|
-
@manifest_file ||= AppFile.new(self, path)
|
67
|
+
@manifest_file ||= AppFile.new(self, path, shared_dir: shared_dir)
|
69
68
|
end
|
70
69
|
|
71
70
|
protected
|
72
71
|
|
73
72
|
# This includes files to assemble... But also subdirectories and commands. This format is
|
74
73
|
# unstable and ugly, and should not be exposed to outside parties who might later depend on it.
|
75
|
-
def assembly_operations(location)
|
74
|
+
def assembly_operations(location, verbose:)
|
76
75
|
operations = []
|
77
76
|
|
78
77
|
raise("No manifest file!") if @no_manifest_file
|
79
78
|
|
80
|
-
|
81
|
-
|
82
|
-
|
79
|
+
# For each spec, put its dependencies before itself in order
|
80
|
+
@manifest_file.ordered_specs.each do |spec|
|
81
|
+
spec_git_repo = spec.source
|
82
|
+
spec_git_repo.use_details(spec.source_details) # This sets things like checked-out branch
|
83
83
|
|
84
84
|
spec.paths.each do |from, to|
|
85
|
-
# Note:
|
86
|
-
from_path = "#{
|
85
|
+
# Note: spec_git_repo.local_dir is an absolute path.
|
86
|
+
from_path = "#{spec_git_repo.local_dir}/#{from}"
|
87
87
|
if File.directory?(from_path)
|
88
88
|
files = Dir["#{from_path}/**/*"].to_a + Dir["#{from_path}/**/.*"].to_a
|
89
89
|
dirs = files.select { |file| File.directory?(file) }
|
@@ -95,13 +95,13 @@ module DGD::Manifest
|
|
95
95
|
no_wild_from_path = components[0..(first_wild_idx-1)].join("/")
|
96
96
|
wild_path = components[first_wild_idx..-1].join("/")
|
97
97
|
|
98
|
-
files = Dir["#{
|
98
|
+
files = Dir["#{spec_git_repo.local_dir}/#{no_wild_from_path}/#{wild_path}"].to_a
|
99
99
|
dirs = files.select { |file| File.directory?(file) }
|
100
100
|
dirs += files.map { |f| File.dirname(f) }
|
101
101
|
dirs.uniq!
|
102
102
|
|
103
103
|
non_dirs = files - dirs
|
104
|
-
operations << { cmd: "cp", from: "#{
|
104
|
+
operations << { cmd: "cp", from: "#{spec_git_repo.local_dir}/#{no_wild_from_path}", to: to, dirs: dirs, non_dirs: non_dirs, comment: :path_wildcard }
|
105
105
|
else
|
106
106
|
# A single file
|
107
107
|
operations << { cmd: "cp", from: from_path, to: to, dirs: [], non_dirs: [from_path], comment: :single_file }
|
@@ -122,16 +122,33 @@ module DGD::Manifest
|
|
122
122
|
|
123
123
|
public
|
124
124
|
|
125
|
-
def
|
126
|
-
|
127
|
-
|
125
|
+
def dgd_root(location)
|
126
|
+
"#{File.expand_path(location)}/#{GENERATED_ROOT}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def assemble_app(location, verbose:)
|
130
|
+
Dir[File.join(dgd_root(location), "*")].each { |dir| FileUtils.rm_rf dir }
|
131
|
+
|
132
|
+
write_app_files(location, verbose: verbose)
|
133
|
+
end
|
134
|
+
|
135
|
+
def update_app(location, verbose:)
|
136
|
+
write_app_files(location, verbose: verbose)
|
137
|
+
end
|
138
|
+
|
139
|
+
protected
|
128
140
|
|
141
|
+
def write_app_files(location, verbose:)
|
129
142
|
Dir.chdir(location) do
|
130
143
|
write_config_file("#{location}/dgd.config")
|
131
144
|
FileUtils.mkdir_p("#{location}/state") # Statedir for statedumps, editor files, etc.
|
132
145
|
|
133
|
-
assembly_operations(location).each do |sd_hash|
|
134
|
-
to_path = "#{dgd_root}/#{sd_hash[:to]}"
|
146
|
+
assembly_operations(location, verbose: verbose).each do |sd_hash|
|
147
|
+
to_path = "#{dgd_root(location)}/#{sd_hash[:to]}"
|
148
|
+
|
149
|
+
if verbose
|
150
|
+
puts " Copy #{sd_hash[:from]} -> #{sd_hash[:to]}, files #{sd_hash[:non_dirs].join(", ")}"
|
151
|
+
end
|
135
152
|
|
136
153
|
# Make appropriate dirs, including empty ones
|
137
154
|
sd_hash[:dirs].each do |dir|
|
@@ -140,17 +157,24 @@ module DGD::Manifest
|
|
140
157
|
|
141
158
|
# Copy all files
|
142
159
|
sd_hash[:non_dirs].each do |from_file|
|
143
|
-
to_file = from_file.sub(sd_hash[:from], "#{dgd_root}/#{sd_hash[:to]}")
|
160
|
+
to_file = from_file.sub(sd_hash[:from], "#{dgd_root(location)}/#{sd_hash[:to]}")
|
144
161
|
to_dir = File.dirname(to_file)
|
145
|
-
|
146
|
-
|
162
|
+
begin
|
163
|
+
FileUtils.mkdir_p to_dir
|
164
|
+
FileUtils.cp from_file, to_file
|
165
|
+
rescue
|
166
|
+
puts "Error when copying: #{from_file} -> #{to_file} in #{sd_hash.inspect}"
|
167
|
+
raise
|
168
|
+
end
|
147
169
|
end
|
148
170
|
end
|
149
171
|
end
|
150
172
|
end
|
151
173
|
|
152
|
-
|
153
|
-
|
174
|
+
public
|
175
|
+
|
176
|
+
def precheck(location, verbose:)
|
177
|
+
all_files = assembly_operations(location, verbose: verbose).flat_map { |sd| sd[:non_dirs] }
|
154
178
|
|
155
179
|
if all_files.size != all_files.uniq.size
|
156
180
|
repeated = all_files.uniq.select { |f| all_files.count(f) > 1 }
|
@@ -160,40 +184,7 @@ module DGD::Manifest
|
|
160
184
|
|
161
185
|
def write_config_file(path)
|
162
186
|
File.open(path, "wb") do |f|
|
163
|
-
f.write
|
164
|
-
/* These are SkotOS limits. They are larger than you are likely to need. They should
|
165
|
-
be configurable but they are not yet. */
|
166
|
-
telnet_port = ([
|
167
|
-
"*":50100 /* telnet port number */
|
168
|
-
]);
|
169
|
-
binary_port = ([
|
170
|
-
"*":50110 /* Failsafe */
|
171
|
-
]); /* binary ports */
|
172
|
-
directory = "./#{GENERATED_ROOT}";
|
173
|
-
|
174
|
-
users = 100; /* max # of users */
|
175
|
-
editors = 40; /* max # of editor sessions */
|
176
|
-
ed_tmpfile = "../state/ed"; /* proto editor tmpfile */
|
177
|
-
swap_file = "../state/swap"; /* swap file */
|
178
|
-
swap_size = 1048576; /* # sectors in swap file */
|
179
|
-
sector_size = 512; /* swap sector size */
|
180
|
-
swap_fragment = 4096; /* fragment to swap out */
|
181
|
-
static_chunk = 64512; /* static memory chunk */
|
182
|
-
dynamic_chunk = 261120; /* dynamic memory chunk */
|
183
|
-
dump_file = "../state/dump"; /* dump file */
|
184
|
-
dump_interval = 3600; /* dump interval */
|
185
|
-
|
186
|
-
typechecking = 2; /* highest level of typechecking */
|
187
|
-
include_file = "/include/std.h"; /* standard include file */
|
188
|
-
include_dirs = ({ "/include", "~/include" }); /* directories to search */
|
189
|
-
auto_object = "/kernel/lib/auto"; /* auto inherited object */
|
190
|
-
driver_object = "/kernel/sys/driver"; /* driver object */
|
191
|
-
create = "_F_create"; /* name of create function */
|
192
|
-
|
193
|
-
array_size = 16384; /* max array size */
|
194
|
-
objects = 262144; /* max # of objects */
|
195
|
-
call_outs = 16384; /* max # of call_outs */
|
196
|
-
CONTENTS
|
187
|
+
f.write @manifest_file.dgd_config.as_file
|
197
188
|
end
|
198
189
|
end
|
199
190
|
end
|
@@ -207,7 +198,7 @@ CONTENTS
|
|
207
198
|
def initialize(repo, git_url)
|
208
199
|
@git_url = git_url
|
209
200
|
@repo = repo
|
210
|
-
local_path = git_url.tr("/\\", "_")
|
201
|
+
local_path = git_url.tr("/\\ ", "_")
|
211
202
|
@local_dir = "#{@repo.shared_dir}/git/#{local_path}"
|
212
203
|
|
213
204
|
if File.directory?(@local_dir)
|
@@ -226,32 +217,33 @@ CONTENTS
|
|
226
217
|
def use_details(details)
|
227
218
|
if details["branch"]
|
228
219
|
Dir.chdir(@local_dir) do
|
229
|
-
DGD::Manifest.system_call("git checkout #{details["branch"]}")
|
220
|
+
DGD::Manifest.system_call("git checkout #{details["branch"]} && git pull")
|
230
221
|
end
|
231
222
|
else
|
232
223
|
Dir.chdir(@local_dir) do
|
233
|
-
DGD::Manifest.system_call("git checkout #{default_branch}")
|
224
|
+
DGD::Manifest.system_call("git checkout #{default_branch} && git pull")
|
234
225
|
end
|
235
226
|
end
|
236
227
|
end
|
237
228
|
end
|
238
229
|
|
230
|
+
# This class parses the DGD manifest
|
239
231
|
class AppFile
|
240
232
|
attr_reader :path
|
241
233
|
attr_reader :repo
|
242
234
|
attr_reader :specs
|
243
|
-
attr_reader :
|
235
|
+
attr_reader :dgd_config
|
236
|
+
attr_reader :shared_dir
|
244
237
|
|
245
|
-
def initialize(repo, path)
|
238
|
+
def initialize(repo, path, shared_dir:)
|
246
239
|
@path = path
|
247
240
|
@repo = repo
|
241
|
+
@shared_dir = shared_dir
|
248
242
|
raise("No such dgd.manifest file as #{path.inspect}!") unless File.exist?(path)
|
249
243
|
contents = AppFile.parse_manifest_file(path)
|
250
244
|
|
251
245
|
read_manifest_file(contents)
|
252
246
|
|
253
|
-
@app_root = contents["app_root"] || "app"
|
254
|
-
|
255
247
|
output_paths = @specs.flat_map { |s| s.paths.values }
|
256
248
|
unless output_paths == output_paths.uniq
|
257
249
|
repeated_paths = output_paths.select { |p| output_paths.count(p) > 1 }
|
@@ -267,9 +259,9 @@ CONTENTS
|
|
267
259
|
#else
|
268
260
|
# puts "This dgd.manifest needs the default Kernel Library."
|
269
261
|
# # This app has specified no kernellib paths -- add them
|
270
|
-
#
|
262
|
+
# spec_git_repo = @repo.git_repo(DEFAULT_KERNELLIB_URL)
|
271
263
|
# klib_spec = GoodsSpec.new @repo, name: "default Kernel Library",
|
272
|
-
# source:
|
264
|
+
# source: spec_git_repo, paths: KERNEL_PATH_MAP
|
273
265
|
# specs.unshift klib_spec
|
274
266
|
#end
|
275
267
|
|
@@ -298,6 +290,12 @@ CONTENTS
|
|
298
290
|
|
299
291
|
@specs = []
|
300
292
|
|
293
|
+
@dgd_config = DGDRuntimeConfig.new (contents["config"] || {})
|
294
|
+
|
295
|
+
if contents["app_root"]
|
296
|
+
raise "App_root must now be inside config block!"
|
297
|
+
end
|
298
|
+
|
301
299
|
if contents["unbundled_goods"]
|
302
300
|
raise "Unbundled_goods must be an array!" unless contents["unbundled_goods"].is_a?(Array)
|
303
301
|
|
@@ -309,7 +307,10 @@ CONTENTS
|
|
309
307
|
|
310
308
|
@specs += contents["goods"].map do |goods_url|
|
311
309
|
begin
|
312
|
-
|
310
|
+
text_contents = URI.open(goods_url).read
|
311
|
+
local_path = shared_dir + "/goods/" + goods_url.tr("/\\ ", "_")
|
312
|
+
File.open(local_path, "wb") { |f| f.write(text_contents) }
|
313
|
+
json_contents = JSON.parse text_contents
|
313
314
|
rescue
|
314
315
|
STDERR.puts "Error reading or parsing by URL: #{goods_url.inspect}"
|
315
316
|
raise
|
@@ -319,9 +320,15 @@ CONTENTS
|
|
319
320
|
end
|
320
321
|
end
|
321
322
|
|
323
|
+
def app_root
|
324
|
+
@dgd_config.app_root
|
325
|
+
end
|
326
|
+
|
322
327
|
def unbundled_json_to_spec(fields)
|
323
328
|
source = nil
|
324
329
|
source_details = nil
|
330
|
+
dependencies = []
|
331
|
+
|
325
332
|
if fields["git"]
|
326
333
|
raise "A git source requires a git url: #{fields.inspect}!" unless fields["git"]["url"]
|
327
334
|
source = @repo.git_repo(fields["git"]["url"])
|
@@ -334,9 +341,46 @@ CONTENTS
|
|
334
341
|
raise "Paths in Goods files must map strings to strings! #{fields["paths"].inspect}"
|
335
342
|
end
|
336
343
|
|
337
|
-
|
344
|
+
if fields["dependencies"]
|
345
|
+
# For now, permit a single string as a dependency.
|
346
|
+
fields["dependencies"] = [ fields["dependencies"] ] if fields["dependencies"].is_a?(String)
|
347
|
+
|
348
|
+
goods_url = nil
|
349
|
+
fields["dependencies"].each do |dep|
|
350
|
+
if dep.is_a?(String)
|
351
|
+
goods_url = dep
|
352
|
+
elsif dep.is_a?(Hash)
|
353
|
+
raise "Currently only URL-based dependencies on Goods files are supported!" unless dep["url"]
|
354
|
+
goods_url = dep["url"]
|
355
|
+
else
|
356
|
+
raise "Unexpected dependency type #{dep.class} when parsing DGD Manifest specs, item: #{dep.inspect}"
|
357
|
+
end
|
358
|
+
|
359
|
+
text_contents = URI.open(goods_url).read
|
360
|
+
local_path = shared_dir + "/goods/" + goods_url.tr("/\\ ", "_")
|
361
|
+
File.open(local_path, "wb") { |f| f.write(text_contents) }
|
362
|
+
dep_fields = JSON.parse text_contents
|
363
|
+
|
364
|
+
dependencies.push unbundled_json_to_spec(dep_fields)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
spec = GoodsSpec.new(@repo, name: fields["name"], source: source, source_details: source_details, paths: fields["paths"], dependencies: dependencies)
|
338
369
|
return spec
|
339
370
|
end
|
371
|
+
|
372
|
+
def ordered_specs
|
373
|
+
@specs.flat_map do |s|
|
374
|
+
deps = [s]
|
375
|
+
deps_to_add = s.dependencies
|
376
|
+
while(deps_to_add.size > 0)
|
377
|
+
next_deps = deps_to_add.flat_map { |dep| dep.dependencies }
|
378
|
+
deps = deps_to_add + deps
|
379
|
+
deps_to_add = next_deps
|
380
|
+
end
|
381
|
+
deps
|
382
|
+
end
|
383
|
+
end
|
340
384
|
end
|
341
385
|
|
342
386
|
class GoodsSpec
|
@@ -345,8 +389,9 @@ CONTENTS
|
|
345
389
|
attr_reader :source
|
346
390
|
attr_reader :source_details
|
347
391
|
attr_reader :paths
|
392
|
+
attr_reader :dependencies
|
348
393
|
|
349
|
-
def initialize(repo, name:, source:, source_details: {}, paths:)
|
394
|
+
def initialize(repo, name:, source:, source_details: {}, paths:, dependencies:)
|
350
395
|
@repo = repo
|
351
396
|
@name = name
|
352
397
|
@source = source
|
@@ -359,6 +404,7 @@ CONTENTS
|
|
359
404
|
end
|
360
405
|
|
361
406
|
@paths = cleaned_paths
|
407
|
+
@dependencies = dependencies
|
362
408
|
end
|
363
409
|
end
|
364
410
|
|
@@ -395,7 +441,7 @@ CONTENTS
|
|
395
441
|
"app_root": "app",
|
396
442
|
"goods": [
|
397
443
|
"# This is an example goods file - substitute your own.",
|
398
|
-
"https://raw.githubusercontent.com/
|
444
|
+
"https://raw.githubusercontent.com/ChatTheatre/dgd-tools/main/goods/skotos_httpd.goods"
|
399
445
|
],
|
400
446
|
"unbundled_goods": [
|
401
447
|
{
|
@@ -455,4 +501,117 @@ FILE_CONTENTS
|
|
455
501
|
puts "Successfully created project at #{@location}."
|
456
502
|
end
|
457
503
|
end
|
504
|
+
|
505
|
+
class DGDRuntimeConfig
|
506
|
+
attr_reader :app_root
|
507
|
+
|
508
|
+
DEFAULT_CONFIG = {
|
509
|
+
users: 100,
|
510
|
+
editors: 40,
|
511
|
+
swap_size: 1048576,
|
512
|
+
sector_size: 512,
|
513
|
+
swap_fragment: 4096,
|
514
|
+
static_chunk: 64512,
|
515
|
+
dynamic_chunk: 261120,
|
516
|
+
dump_interval: 3600,
|
517
|
+
typechecking: 2,
|
518
|
+
include_file: "/include/std.h",
|
519
|
+
include_dirs: ["/include", "~/include"],
|
520
|
+
auto_object: "/kernel/lib/auto",
|
521
|
+
driver_object: "/kernel/sys/driver",
|
522
|
+
create: "_F_create",
|
523
|
+
array_size: 16384,
|
524
|
+
objects: 262144,
|
525
|
+
call_outs: 16384,
|
526
|
+
}
|
527
|
+
CONFIG_KEYS = DEFAULT_CONFIG.keys.map(&:to_s) + [ "app_root", "ports", "telnet_ports", "dump_file", "statedir" ]
|
528
|
+
|
529
|
+
def initialize(config_data)
|
530
|
+
@app_root = config_data["app_root"] || "app"
|
531
|
+
@ports = {
|
532
|
+
"*" => 50100,
|
533
|
+
}
|
534
|
+
@telnet_ports = {
|
535
|
+
"*" => 50110,
|
536
|
+
}
|
537
|
+
@statedir = config_data["statedir"] || "state"
|
538
|
+
@dump_file = if config_data["dump_file"]
|
539
|
+
"../" + config_data["dump_file"]
|
540
|
+
else
|
541
|
+
"../#{@statedir}/dump"
|
542
|
+
end
|
543
|
+
@config = DEFAULT_CONFIG.dup
|
544
|
+
|
545
|
+
@raw_data = config_data
|
546
|
+
@config.keys.each do |prop|
|
547
|
+
# For now, assume and require that JSON data is the correct type if present
|
548
|
+
@config[prop] = config_data[prop.to_s] if config_data[prop.to_s]
|
549
|
+
end
|
550
|
+
unexpected_config_keys = config_data.keys - CONFIG_KEYS
|
551
|
+
unless unexpected_config_keys.empty?
|
552
|
+
raise "Unexpected key names in DGD configuration: #{unexpected_config_keys.inspect}!"
|
553
|
+
end
|
554
|
+
|
555
|
+
if config_data["telnet_ports"]
|
556
|
+
@telnet_ports = config_to_ports(config_data["telnet_ports"])
|
557
|
+
end
|
558
|
+
if config_data["ports"]
|
559
|
+
@ports = config_to_ports(config_data["ports"])
|
560
|
+
end
|
561
|
+
end
|
562
|
+
|
563
|
+
def config_to_ports(data)
|
564
|
+
if data.is_a?(Hash)
|
565
|
+
return data.map { |ip, port| [ip, Integer(port) ] }
|
566
|
+
elsif data.is_a?(Array)
|
567
|
+
if data[0].is_a?(Array)
|
568
|
+
ports = data.map { |ip, port| [ip, Integer(port) ] }
|
569
|
+
return ports
|
570
|
+
end
|
571
|
+
|
572
|
+
ports = data.map { |p| [ "*", Integer(p) ] }
|
573
|
+
STDERR.puts "Arrayified: #{ports.inspect}"
|
574
|
+
return ports
|
575
|
+
elsif data.is_a?(Integer)
|
576
|
+
return [ [ "*", data ] ]
|
577
|
+
else
|
578
|
+
raise "dgd-manifest: not sure how to get port data from a #{data.class.name} -- #{data.inspect}!"
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
def as_file
|
583
|
+
return <<DGD_CONFIG
|
584
|
+
telnet_port = ([
|
585
|
+
#{@telnet_ports.map { |ip, p| "#{ip.inspect}:#{p}" }.join(",\n ") }
|
586
|
+
]); /* legacy telnet ports */
|
587
|
+
binary_port = ([
|
588
|
+
#{@ports.map { |ip, p| "#{ip.inspect}:#{p}" }.join(",\n ") }
|
589
|
+
]); /* binary ports */
|
590
|
+
directory = "./#{GENERATED_ROOT}";
|
591
|
+
|
592
|
+
users = #{@config[:users]}; /* max # of connections */
|
593
|
+
editors = #{@config[:editors]}; /* max # of built-in-editor sessions */
|
594
|
+
ed_tmpfile = "../#{@statedir}/ed"; /* proto editor tmpfile */
|
595
|
+
swap_file = "../#{@statedir}/swap"; /* swap file */
|
596
|
+
swap_size = #{@config[:swap_size]}; /* # sectors in swap file */
|
597
|
+
sector_size = #{@config[:sector_size]}; /* swap sector size */
|
598
|
+
swap_fragment = #{@config[:swap_fragment]}; /* fragment to swap out */
|
599
|
+
static_chunk = #{@config[:static_chunk]}; /* static memory chunk */
|
600
|
+
dynamic_chunk = #{@config[:dynamic_chunk]}; /* dynamic memory chunk */
|
601
|
+
dump_file = #{@dump_file.inspect}; /* dump file */
|
602
|
+
dump_interval = #{@config[:dump_interval]}; /* expected statedump interval in seconds */
|
603
|
+
|
604
|
+
typechecking = #{@config[:typechecking]}; /* level of typechecking (2 is highest) */
|
605
|
+
include_file = #{@config[:include_file].inspect}; /* standard include file */
|
606
|
+
include_dirs = ({ #{@config[:include_dirs].map(&:inspect).join(", ")} }); /* directories to search */
|
607
|
+
auto_object = #{@config[:auto_object].inspect}; /* auto inherited object */
|
608
|
+
driver_object = #{@config[:driver_object].inspect}; /* driver object */
|
609
|
+
create = #{@config[:create].inspect}; /* name of create function */
|
610
|
+
|
611
|
+
array_size = #{@config[:array_size]}; /* max array size */
|
612
|
+
objects = #{@config[:objects]}; /* max # of objects */
|
613
|
+
call_outs = #{@config[:call_outs]}; /* max # of callouts */
|
614
|
+
DGD_CONFIG
|
615
|
+
end
|
616
|
+
end
|
458
617
|
end
|
@@ -8,13 +8,21 @@ require "tempfile"
|
|
8
8
|
|
9
9
|
module SkotOS; end
|
10
10
|
|
11
|
-
|
11
|
+
class SkotOS::XMLObject; end
|
12
|
+
|
13
|
+
class << SkotOS::XMLObject
|
14
|
+
attr_accessor :merry_only
|
15
|
+
attr_accessor :ignore_whitespace
|
16
|
+
attr_accessor :ignore_types
|
17
|
+
end
|
12
18
|
|
13
19
|
class SkotOS::XMLObject
|
14
20
|
attr_reader :pretty
|
21
|
+
attr_reader :noko_doc
|
15
22
|
|
16
|
-
def initialize(pretty)
|
23
|
+
def initialize(pretty, noko_doc: nil)
|
17
24
|
@pretty = pretty
|
25
|
+
@noko_doc = noko_doc
|
18
26
|
end
|
19
27
|
|
20
28
|
def self.from_file(filename)
|
@@ -25,9 +33,7 @@ class SkotOS::XMLObject
|
|
25
33
|
remove_undiffed(doc)
|
26
34
|
|
27
35
|
pretty = doc.to_xml(indent:3)
|
28
|
-
|
29
|
-
#prune_whitespace(data)
|
30
|
-
SkotOS::XMLObject.new pretty
|
36
|
+
SkotOS::XMLObject.new pretty, noko_doc: doc
|
31
37
|
end
|
32
38
|
|
33
39
|
def self.diff_between(obj1, obj2, o1_name: "Object 1", o2_name: "Object 2")
|
@@ -40,8 +46,13 @@ class SkotOS::XMLObject
|
|
40
46
|
of1.close
|
41
47
|
of2.close
|
42
48
|
|
49
|
+
diff_opts = [ "c" ]
|
50
|
+
diff_opts += [ "b", "B" ] if self.ignore_whitespace
|
51
|
+
|
43
52
|
# Diff 'fails' if there's a difference between the two files.
|
44
|
-
|
53
|
+
cmd = "diff -#{diff_opts.join("")} #{of1.path} #{of2.path}"
|
54
|
+
#puts "Diff command: #{cmd}"
|
55
|
+
diff = system_call(cmd, fail_ok: true)
|
45
56
|
diff.sub!(of1.path, o1_name)
|
46
57
|
diff.sub!(of2.path, o2_name)
|
47
58
|
ensure
|
@@ -51,28 +62,35 @@ class SkotOS::XMLObject
|
|
51
62
|
diff
|
52
63
|
end
|
53
64
|
|
54
|
-
def self.skip_ignored_files(list)
|
55
|
-
|
56
|
-
|
57
|
-
|
65
|
+
def self.skip_ignored_files(list, base_dir)
|
66
|
+
if self.merry_only
|
67
|
+
list.select { |path| File.directory?(base_dir + "/" + path) ||
|
68
|
+
path[/.xml$/] || path[/.XML$/] }
|
69
|
+
else
|
70
|
+
list.select do |path|
|
71
|
+
!path[/,v$/] && # Ignore files ending in comma-v
|
72
|
+
!path[/-backup-\d+-\d+-\d+\.xml/] && # Ignore files ending in -backup-[DATE].xml
|
73
|
+
path != ".git" && # Ignore .git directories
|
74
|
+
path != "MOVED" # Ignore MOVED - it's a sort of recycle, waiting to be emptied
|
75
|
+
end
|
58
76
|
end
|
59
77
|
end
|
60
78
|
|
61
79
|
def self.diff_dirs(dir1, dir2)
|
62
|
-
entries1 = skip_ignored_files(Dir.glob("*", base: dir1).to_a)
|
63
|
-
entries2 = skip_ignored_files(Dir.glob("*", base: dir2).to_a)
|
80
|
+
entries1 = skip_ignored_files(Dir.glob("*", base: dir1).to_a, dir1)
|
81
|
+
entries2 = skip_ignored_files(Dir.glob("*", base: dir2).to_a, dir2)
|
64
82
|
|
65
83
|
only_in_1 = entries1 - entries2
|
66
84
|
only_in_2 = entries2 - entries1
|
67
85
|
in_both = entries1 & entries2
|
68
86
|
|
69
87
|
diff = []
|
70
|
-
diff << "Only in first: #{only_in_1.join(", ")}" unless only_in_1.empty?
|
71
|
-
diff << "Only in second: #{only_in_2.join(", ")}" unless only_in_2.empty?
|
88
|
+
diff << "Only in first: #{only_in_1.map { |s| dir1 + "/" + s }.join(", ")}" unless only_in_1.empty?
|
89
|
+
diff << "Only in second: #{only_in_2.map { |s| dir2 + "/" + s }.join(", ")}" unless only_in_2.empty?
|
72
90
|
|
73
91
|
in_both.each do |file|
|
74
|
-
in_1 =
|
75
|
-
in_2 =
|
92
|
+
in_1 = File.join dir1, file
|
93
|
+
in_2 = File.join dir2, file
|
76
94
|
if File.directory?(in_1) ^ File.directory?(in_2)
|
77
95
|
diff << "Only a directory in one, not both: #{dir1}/#{file}"
|
78
96
|
elsif File.directory?(in_1)
|
@@ -97,6 +115,101 @@ class SkotOS::XMLObject
|
|
97
115
|
end
|
98
116
|
end
|
99
117
|
end
|
118
|
+
|
119
|
+
rev = noko_single_node(doc.root, "Core:Property", attrs: { "property" => "revisions" })
|
120
|
+
noko_remove(rev) if rev
|
121
|
+
|
122
|
+
list = noko_single_node(doc.root, "Core:Property", attrs: { "property" => "#list#" })
|
123
|
+
list.remove if list
|
124
|
+
|
125
|
+
properties = noko_with_name_and_attrs(doc.root, "Core:Property")
|
126
|
+
properties.each do |prop_node|
|
127
|
+
prop_node.remove if prop_node.attribute("property").value.start_with?("sys:sync")
|
128
|
+
end
|
129
|
+
|
130
|
+
if self.merry_only
|
131
|
+
# Kill off all the non-Merry nodes
|
132
|
+
noko_remove_non_merry_nodes(doc.root)
|
133
|
+
end
|
134
|
+
|
135
|
+
if self.ignore_types
|
136
|
+
self.ignore_types.each do |ignored_type|
|
137
|
+
skipped = noko_with_name_and_attrs(doc.root, ignored_type)
|
138
|
+
skipped.each { |n| noko_remove(n) }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
base_combat = noko_single_node(doc.root, "Base:Combat")
|
143
|
+
if base_combat
|
144
|
+
base_strength = noko_single_node(base_combat, "Base:Strength", attrs: { "value" => "1" })
|
145
|
+
base_max_fatigue = noko_single_node(base_combat, "Base:MaxFatigue", attrs: { "value" => "1" })
|
146
|
+
if base_strength && base_max_fatigue && noko_non_text(base_combat.children).size == 2
|
147
|
+
next_text = base_combat.next
|
148
|
+
base_combat.remove
|
149
|
+
next_text.remove
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.noko_remove(node)
|
155
|
+
nn = node.next
|
156
|
+
nn.remove if nn.is_a?(Nokogiri::XML::Text)
|
157
|
+
node.remove
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.noko_single_node(node, name, attrs: {})
|
161
|
+
choices = noko_with_name_and_attrs(node, name, attrs)
|
162
|
+
if choices.size < 1
|
163
|
+
nil
|
164
|
+
elsif choices.size > 1
|
165
|
+
raise "Single-node search returned more than one node! #{name.inspect}, #{attrs.inspect}"
|
166
|
+
else
|
167
|
+
choices[0]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.noko_non_text(nodes)
|
172
|
+
nodes.select { |n| !n.is_a? Nokogiri::XML::Text }
|
173
|
+
end
|
174
|
+
|
175
|
+
def self.noko_with_name_and_attrs(node, name, attrs = {})
|
176
|
+
results = node.children.flat_map { |n| noko_with_name_and_attrs(n, name, attrs) }
|
177
|
+
if node.name == name &&
|
178
|
+
attrs.all? { |k, v| node.attribute(k).value == v }
|
179
|
+
results << node
|
180
|
+
end
|
181
|
+
results
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.noko_remove_non_merry_nodes(root)
|
185
|
+
root.children.each do |node|
|
186
|
+
if node.name != "Core:PropertyContainer"
|
187
|
+
node.remove
|
188
|
+
next
|
189
|
+
end
|
190
|
+
|
191
|
+
node.children.each do |node2|
|
192
|
+
if node2.name != "Core:PCProperties"
|
193
|
+
node2.remove
|
194
|
+
next
|
195
|
+
end
|
196
|
+
|
197
|
+
node2.children.each do |property_node|
|
198
|
+
if property_node.name != "Core:Property" || property_node.attribute("property").value[0..5] != "merry:"
|
199
|
+
property_node.remove
|
200
|
+
next
|
201
|
+
end
|
202
|
+
# Leave the Merry node alone
|
203
|
+
end
|
204
|
+
|
205
|
+
if node2.children.size == 0
|
206
|
+
node2.remove
|
207
|
+
end
|
208
|
+
end
|
209
|
+
if node.children.size == 0
|
210
|
+
node.remove
|
211
|
+
end
|
212
|
+
end
|
100
213
|
end
|
101
214
|
|
102
215
|
def self.system_call(cmd, fail_ok: false)
|
data/lib/dgd-tools/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dgd-tools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.11
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Gibbs
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-03-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.10.5
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: optimist
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.0.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.0.1
|
27
41
|
description: dgd-tools supplies DGD Manifest and eventually perhaps other tools. DGD
|
28
42
|
Manifest is an experimental DGD library and packaging system.
|
29
43
|
email:
|
@@ -53,6 +67,7 @@ files:
|
|
53
67
|
- example_xml/t2/Thing.xml
|
54
68
|
- exe/dgd-manifest
|
55
69
|
- exe/skotos-xml-diff
|
70
|
+
- goods/chattheatre_kernellib.goods
|
56
71
|
- goods/skotos_httpd.goods
|
57
72
|
- lib/dgd-tools/manifest.rb
|
58
73
|
- lib/dgd-tools/skotos_xml_obj.rb
|