prick 0.19.0 → 0.20.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.
- checksums.yaml +4 -4
- data/Gemfile +10 -4
- data/README.md +7 -7
- data/Rakefile +3 -1
- data/TODO +13 -11
- data/bin/console +2 -1
- data/doc/build-yml.txt +14 -0
- data/exe/prick +237 -19
- data/lib/builder/batch.rb +147 -0
- data/lib/builder/builder.rb +122 -0
- data/lib/builder/node.rb +189 -0
- data/lib/builder/node_pool.rb +105 -0
- data/lib/builder/parser.rb +120 -0
- data/lib/local/command.rb +193 -0
- data/lib/{prick → local}/git.rb +148 -22
- data/lib/local/timer.rb +98 -0
- data/lib/prick/constants.rb +54 -66
- data/lib/prick/diff.rb +28 -18
- data/lib/prick/prick_version.rb +161 -0
- data/lib/prick/state.rb +80 -165
- data/lib/prick/version.rb +2 -163
- data/lib/prick.rb +38 -24
- data/lib/share/init/.gitignore +10 -0
- data/lib/share/init/.prick-context +2 -0
- data/lib/share/init/.rspec +3 -0
- data/{share/schema/schema/public → lib/share/init/migration}/.keep +0 -0
- data/lib/share/init/prick.yml +6 -0
- data/lib/share/init/schema/.keep +0 -0
- data/lib/share/init/schema/build.yml +2 -0
- data/lib/share/init/schema/prick/.keep +0 -0
- data/lib/share/init/schema/prick/build.yml +5 -0
- data/lib/share/init/schema/prick/data.sql +6 -0
- data/{share/schema → lib/share/init}/schema/prick/tables.sql +2 -3
- data/lib/share/init/schema/public/.keep +0 -0
- data/lib/share/init/spec/prick_helper.rb +1 -0
- data/lib/share/init/spec/prick_spec.rb +6 -0
- data/lib/share/init/spec/spec_helper.rb +50 -0
- data/lib/share/migrate/migration/build.yml +4 -0
- data/lib/share/migrate/migration/diff.after-tables.sql +0 -0
- data/lib/share/migrate/migration/diff.before-tables.sql +0 -0
- data/lib/share/migrate/migration/diff.tables.sql +0 -0
- data/lib/subcommand/prick-build.rb +55 -0
- data/lib/subcommand/prick-create.rb +78 -0
- data/lib/subcommand/prick-drop.rb +25 -0
- data/lib/subcommand/prick-fox.rb +62 -0
- data/lib/subcommand/prick-init.rb +46 -0
- data/lib/subcommand/prick-make.rb +202 -0
- data/lib/subcommand/prick-migrate.rb +37 -0
- data/lib/subcommand/prick-release.rb +23 -0
- data/lib/subcommand/prick-setup.rb +20 -0
- data/lib/subcommand/prick-teardown.rb +18 -0
- data/prick.gemspec +32 -21
- metadata +95 -76
- data/.gitignore +0 -29
- data/.travis.yml +0 -7
- data/doc/create_release.txt +0 -17
- data/doc/flow.txt +0 -98
- data/doc/migra +0 -1
- data/doc/migrations.txt +0 -172
- data/doc/notes.txt +0 -116
- data/doc/prick.txt +0 -114
- data/doc/sh.prick +0 -316
- data/lib/ext/algorithm.rb +0 -14
- data/lib/ext/fileutils.rb +0 -26
- data/lib/ext/forward_method.rb +0 -18
- data/lib/ext/pg.rb +0 -18
- data/lib/ext/shortest_path.rb +0 -44
- data/lib/prick/archive.rb +0 -124
- data/lib/prick/branch.rb +0 -265
- data/lib/prick/builder.rb +0 -246
- data/lib/prick/cache.rb +0 -34
- data/lib/prick/command.rb +0 -104
- data/lib/prick/database.rb +0 -82
- data/lib/prick/dsort.rb +0 -151
- data/lib/prick/ensure.rb +0 -119
- data/lib/prick/exceptions.rb +0 -25
- data/lib/prick/head.rb +0 -189
- data/lib/prick/migration.rb +0 -70
- data/lib/prick/program.rb +0 -287
- data/lib/prick/project.rb +0 -626
- data/lib/prick/rdbms.rb +0 -137
- data/lib/prick/schema.rb +0 -27
- data/lib/prick/share.rb +0 -64
- data/libexec/strip-comments +0 -33
- data/make_releases +0 -72
- data/make_schema +0 -10
- data/share/diff/diff.after-tables.sql +0 -4
- data/share/diff/diff.before-tables.sql +0 -4
- data/share/diff/diff.tables.sql +0 -8
- data/share/features/diff.sql +0 -2
- data/share/features/feature/diff.sql +0 -2
- data/share/features/feature/migrate.sql +0 -2
- data/share/features/features.sql +0 -2
- data/share/features/features.yml +0 -2
- data/share/features/migrations.sql +0 -4
- data/share/gitignore +0 -2
- data/share/migration/diff.tables.sql +0 -8
- data/share/migration/features.yml +0 -6
- data/share/migration/migrate.sql +0 -3
- data/share/migration/migrate.yml +0 -8
- data/share/migration/tables.sql +0 -3
- data/share/schema/build.yml +0 -14
- data/share/schema/schema/build.yml +0 -3
- data/share/schema/schema/prick/build.yml +0 -14
- data/share/schema/schema/prick/data.sql +0 -7
- data/share/schema/schema/prick/schema.sql +0 -3
- data/share/schema/schema/public/build.yml +0 -13
- data/share/schema/schema.sql +0 -3
- data/test_assorted +0 -192
- data/test_feature +0 -112
- data/test_refactor +0 -34
- data/test_single_dev +0 -83
data/lib/builder/node.rb
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
module Prick
|
|
2
|
+
module Build
|
|
3
|
+
class Node
|
|
4
|
+
attr_reader :parent
|
|
5
|
+
forward_to :parent, :conn
|
|
6
|
+
attr_reader :phase # :init, :decl, :seed, :term or nil (for BuildNode)
|
|
7
|
+
attr_reader :kind # :sql, :exe, :fox, :yml, :inline, :module
|
|
8
|
+
attr_reader :path
|
|
9
|
+
attr_reader :args # only defined for :exe (String)
|
|
10
|
+
|
|
11
|
+
def name() @name = File.basename(path) end
|
|
12
|
+
|
|
13
|
+
def schema() @schema ||= parent&.schema || "public" end
|
|
14
|
+
def schema=(s) @schema = s end
|
|
15
|
+
|
|
16
|
+
attr_reader :source
|
|
17
|
+
|
|
18
|
+
def source
|
|
19
|
+
@source ||= read_source
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def prefix_lines() 0 end
|
|
23
|
+
|
|
24
|
+
def source_lines()
|
|
25
|
+
return @source_lines if @source_lines
|
|
26
|
+
source
|
|
27
|
+
@source_lines
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lines() prefix_lines + source_lines end
|
|
31
|
+
|
|
32
|
+
def initialize(parent, phase, kind, path, args = nil)
|
|
33
|
+
constrain parent, BuildNode, NilClass
|
|
34
|
+
constrain phase, :init, :decl, :seed, :term, nil
|
|
35
|
+
constrain kind, :sql, :exe, :fox, :yml, :inline, :module
|
|
36
|
+
constrain path, String, NilClass
|
|
37
|
+
@parent, @phase, @kind, @path = parent, phase, kind, path
|
|
38
|
+
@args = args&.empty? ? nil : args
|
|
39
|
+
@schema = nil
|
|
40
|
+
@source_lines = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_s() [path, args].compact.join(" ") end
|
|
44
|
+
def inspect() to_s end
|
|
45
|
+
def dump() puts "#{inspect} (#{schema})" end
|
|
46
|
+
|
|
47
|
+
protected
|
|
48
|
+
def read_source
|
|
49
|
+
@source_lines = 0
|
|
50
|
+
@source = []
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class SqlNode < Node
|
|
55
|
+
def initialize(parent, phase, path)
|
|
56
|
+
super(parent, phase, :sql, path)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
protected
|
|
60
|
+
def read_source
|
|
61
|
+
file = File.read(path)
|
|
62
|
+
@source_lines = 1 + 1 + file.count("\n")
|
|
63
|
+
["set search_path to #{schema};\n", file]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class FoxNode < Node
|
|
68
|
+
def initialize(parent, phase, path)
|
|
69
|
+
super(parent, phase, :fox, path)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Note that #path refers to the build file
|
|
74
|
+
class InlineNode < Node
|
|
75
|
+
attr_reader :stmts
|
|
76
|
+
|
|
77
|
+
def initialize(parent, phase, path, *stmts)
|
|
78
|
+
super(parent, phase, :inline, path)
|
|
79
|
+
@stmts = Array(stmts).flatten
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def inspect() "#@path \"#{@stmts.join(";")}\"" end
|
|
83
|
+
|
|
84
|
+
protected
|
|
85
|
+
def read_source
|
|
86
|
+
@source_lines = @stmts.size
|
|
87
|
+
@stmts
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class ModuleNode < Node
|
|
92
|
+
attr_reader :klass
|
|
93
|
+
attr_reader :command
|
|
94
|
+
def object() self.class.objects[klass] end
|
|
95
|
+
|
|
96
|
+
def initialize(parent, phase, path, klass, command, args = nil)
|
|
97
|
+
constrain klass, Symbol, String
|
|
98
|
+
constrain command, Symbol, String
|
|
99
|
+
|
|
100
|
+
super(parent, phase, :module, path, args)
|
|
101
|
+
|
|
102
|
+
@klass = klass.to_sym
|
|
103
|
+
@command = command.to_sym
|
|
104
|
+
|
|
105
|
+
if !object
|
|
106
|
+
Kernel.class_eval File.read(path)
|
|
107
|
+
self.class.objects[@klass] = eval(klass.to_s).new(conn)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def call()
|
|
112
|
+
object.send(@command, *args)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def inspect() "#{path} #{klass}##{command}" end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
# Map from klass name (a Symbol) to object
|
|
119
|
+
@@objects = {}
|
|
120
|
+
def self.objects() @@objects end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class ExeNode < Node
|
|
124
|
+
# Using a pipe instead of just executing the command shaves off some
|
|
125
|
+
# deciseconds spent starting up bash. It expects the process to read
|
|
126
|
+
# database/username from standard input
|
|
127
|
+
attr_reader :pipe
|
|
128
|
+
|
|
129
|
+
def initialize(parent, phase, path, args = nil)
|
|
130
|
+
super(parent, phase, :exe, path, args)
|
|
131
|
+
@pipe = Command::Pipe.new(to_s, stderr: nil)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def inspect() "#{path}(#{args.join(", ")})" end
|
|
135
|
+
|
|
136
|
+
protected
|
|
137
|
+
def read_source
|
|
138
|
+
pipe.puts [conn.name, conn.user]
|
|
139
|
+
sql = pipe.wait
|
|
140
|
+
@source_lines = 1 + 1 + sql.count("\n")
|
|
141
|
+
["set search_path to #{schema};\n", sql]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class BuildNode < Node
|
|
146
|
+
def nodes() @nodes ||= init_nodes + decl_nodes + seed_nodes + term_nodes end
|
|
147
|
+
|
|
148
|
+
attr_reader :decl_nodes
|
|
149
|
+
attr_reader :init_nodes
|
|
150
|
+
attr_reader :term_nodes
|
|
151
|
+
attr_reader :seed_nodes
|
|
152
|
+
|
|
153
|
+
def initialize(parent, path)
|
|
154
|
+
super(parent, nil, :yml, path)
|
|
155
|
+
@decl_nodes = []
|
|
156
|
+
@init_nodes = []
|
|
157
|
+
@term_nodes = []
|
|
158
|
+
@seed_nodes = []
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def inspect() to_s end
|
|
162
|
+
|
|
163
|
+
def dump
|
|
164
|
+
puts "BuildNode #{path}"
|
|
165
|
+
indent {
|
|
166
|
+
puts "schema: #{schema}" if schema
|
|
167
|
+
decl_nodes.each(&:dump)
|
|
168
|
+
for kind in [:init, :term, :seed]
|
|
169
|
+
kind_nodes = self.send("#{kind}_nodes".to_sym)
|
|
170
|
+
if !kind_nodes.empty?
|
|
171
|
+
puts "#{kind.upcase}:"
|
|
172
|
+
indent { kind_nodes.each(&:dump) }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class RootBuildNode < BuildNode
|
|
180
|
+
attr_reader :conn
|
|
181
|
+
|
|
182
|
+
def initialize(conn, path)
|
|
183
|
+
@conn = conn
|
|
184
|
+
super(nil, path)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
module Prick
|
|
2
|
+
module Build
|
|
3
|
+
class NodePool
|
|
4
|
+
def schemas() @schemas.keys end
|
|
5
|
+
def before_schema(s) schemas.take_while { |schema| schema != s } end
|
|
6
|
+
def after_schema(s) schemas.reverse.take_while { |schema| schema != s }.reverse end
|
|
7
|
+
|
|
8
|
+
attr_reader :nodes
|
|
9
|
+
|
|
10
|
+
attr_reader :init_nodes
|
|
11
|
+
attr_reader :decl_nodes
|
|
12
|
+
attr_reader :seed_nodes
|
|
13
|
+
attr_reader :term_nodes
|
|
14
|
+
|
|
15
|
+
# attr_reader :setup_nodes
|
|
16
|
+
# attr_reader :teardown_nodes
|
|
17
|
+
|
|
18
|
+
def fox_seed_nodes() seed_nodes.select { |node| node.kind == :fox } end
|
|
19
|
+
def sql_seed_nodes() seed_nodes.select { |node| node.kind == :sql } end
|
|
20
|
+
|
|
21
|
+
def initialize()
|
|
22
|
+
self.clear
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add(*nodes)
|
|
26
|
+
nodes = Array(nodes).flatten
|
|
27
|
+
@nodes.concat(nodes)
|
|
28
|
+
nodes.each { |node|
|
|
29
|
+
@schemas[node.schema] += 1
|
|
30
|
+
@kind_nodes[node.phase]&.append(node)
|
|
31
|
+
}
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(*nodes)
|
|
36
|
+
# puts "#delete(*nodes)"
|
|
37
|
+
nodes = Array(nodes).flatten
|
|
38
|
+
nodes.each { |node|
|
|
39
|
+
delete_node(node)
|
|
40
|
+
kind_nodes = @kind_nodes[node.phase] and kind_nodes.delete_at(kind_nodes.index(node))
|
|
41
|
+
}
|
|
42
|
+
nodes.last
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_if(phase = nil, &block)
|
|
46
|
+
candidates = @kind_nodes[phase] || @nodes
|
|
47
|
+
delete(candidates.select { |node| yield(node) })
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete_schema(*schemas, exclude: [])
|
|
51
|
+
schemas = Array(schemas).flatten
|
|
52
|
+
delete_if { |node|
|
|
53
|
+
schemas.include?(node.schema) && !exclude.include?(node.phase) && !exclude.include?(node.kind)
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def clear(*phases)
|
|
58
|
+
phases = Array(phases).flatten
|
|
59
|
+
if !phases.empty?
|
|
60
|
+
for phase in phases
|
|
61
|
+
nodes = @kind_nodes[phase]
|
|
62
|
+
nodes.each { |node| delete_node(node) }
|
|
63
|
+
@kind_nodes[phase].clear
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
@schemas = Hash.new(0) # map from schema name to number of nodes
|
|
67
|
+
@nodes = []
|
|
68
|
+
@init_nodes = []
|
|
69
|
+
@decl_nodes = []
|
|
70
|
+
@seed_nodes = []
|
|
71
|
+
@term_nodes = []
|
|
72
|
+
@kind_nodes = {
|
|
73
|
+
decl: @decl_nodes,
|
|
74
|
+
init: @init_nodes,
|
|
75
|
+
seed: @seed_nodes,
|
|
76
|
+
term: @term_nodes,
|
|
77
|
+
yml: nil
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def dump
|
|
83
|
+
puts "NodePool, #{nodes.size} nodes"
|
|
84
|
+
indent {
|
|
85
|
+
puts "init_nodes:"
|
|
86
|
+
indent { @init_nodes.each &:dump }
|
|
87
|
+
puts "decl_nodes:"
|
|
88
|
+
indent { @decl_nodes.each &:dump }
|
|
89
|
+
puts "seed_nodes:"
|
|
90
|
+
indent { @seed_nodes.each &:dump }
|
|
91
|
+
puts "term_nodes:"
|
|
92
|
+
indent { @term_nodes.each &:dump }
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
def delete_node(node)
|
|
98
|
+
# puts "#delete_node"
|
|
99
|
+
@nodes.delete_at(@nodes.index(node))
|
|
100
|
+
@schemas[node.schema] -= 1
|
|
101
|
+
@schemas.delete(node.schema) if @schemas[node.schema] == 0
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module Prick
|
|
2
|
+
module Build
|
|
3
|
+
class Parser
|
|
4
|
+
def self.parse(conn, dir)
|
|
5
|
+
Parser.new(conn).parse(dir).unit
|
|
6
|
+
# [parser.unit, parser.schemas]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :conn
|
|
10
|
+
attr_reader :dir
|
|
11
|
+
attr_reader :unit
|
|
12
|
+
# attr_reader :schemas
|
|
13
|
+
|
|
14
|
+
def initialize(conn)
|
|
15
|
+
@conn = conn
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse(dir)
|
|
19
|
+
@dir = dir
|
|
20
|
+
# @schemas = {}
|
|
21
|
+
parse_directory(nil, dir)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# First build unit is a RootBuildNode, the rest are regular BuildNode objects
|
|
28
|
+
def make_build_unit(parent, path)
|
|
29
|
+
if @unit
|
|
30
|
+
BuildNode.new(parent, path)
|
|
31
|
+
else
|
|
32
|
+
@unit = RootBuildNode.new(conn, path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_directory(parent, dir)
|
|
37
|
+
build_file = "#{dir}/build.yml".sub(/\/\//, "/")
|
|
38
|
+
if File.exist? build_file
|
|
39
|
+
parse_build_file(parent, dir, build_file)
|
|
40
|
+
else
|
|
41
|
+
raise Error, "Can't find build.yml in #{dir}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_build_file(parent, dir, path)
|
|
46
|
+
unit = make_build_unit(parent, path)
|
|
47
|
+
entries = YAML.load(File.read(path)) || []
|
|
48
|
+
entries.each { |entry|
|
|
49
|
+
if entry.is_a?(Hash)
|
|
50
|
+
entry.each { |key, value|
|
|
51
|
+
if key == "schema"
|
|
52
|
+
unit.schema = value
|
|
53
|
+
# @schemas[unit.schema = value] = true
|
|
54
|
+
else
|
|
55
|
+
case key
|
|
56
|
+
when "init"; unit.init_nodes
|
|
57
|
+
when "term"; unit.term_nodes
|
|
58
|
+
when "seed"; unit.seed_nodes
|
|
59
|
+
else
|
|
60
|
+
raise Error, "Illegal key in #{unit.path}: #{key}"
|
|
61
|
+
end.concat(Array(value).map { |value| parse_entry(unit, key.to_sym, dir, value) })
|
|
62
|
+
end
|
|
63
|
+
}
|
|
64
|
+
else
|
|
65
|
+
node = parse_entry(unit, :decl, dir, entry)
|
|
66
|
+
if node.kind == :fox
|
|
67
|
+
unit.seed_nodes << node
|
|
68
|
+
else
|
|
69
|
+
unit.decl_nodes << node
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
}
|
|
73
|
+
unit
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_entry(unit, phase, dir, entry)
|
|
77
|
+
if entry.is_a?(Hash)
|
|
78
|
+
entry.size == 1 or raise Error, "sql and module are single-line values"
|
|
79
|
+
key, value = entry.first
|
|
80
|
+
case key
|
|
81
|
+
when "sql"; InlineNode.new(unit, phase, unit.path, value)
|
|
82
|
+
when "call";
|
|
83
|
+
args = value.split(/\s+/)
|
|
84
|
+
args.size >= 3 or raise "Illegal number of arguments: #{value}"
|
|
85
|
+
file, klass, command, args = *args
|
|
86
|
+
ModuleNode.new(unit, phase, "#{dir}/#{file}", klass, command, args)
|
|
87
|
+
else
|
|
88
|
+
raise Error, "Illegal key: #{key}"
|
|
89
|
+
end
|
|
90
|
+
else
|
|
91
|
+
name = entry
|
|
92
|
+
name.sub!(/\/$/, "")
|
|
93
|
+
if name =~ /^(\S+)\s+(.*)$/ # exe
|
|
94
|
+
file = $1
|
|
95
|
+
args = $2
|
|
96
|
+
else
|
|
97
|
+
file = name
|
|
98
|
+
end
|
|
99
|
+
path = "#{dir}/#{file}"
|
|
100
|
+
if File.directory? path
|
|
101
|
+
parse_directory(unit, path)
|
|
102
|
+
elsif File.executable? path
|
|
103
|
+
ExeNode.new(unit, phase, path, args)
|
|
104
|
+
elsif File.file? path
|
|
105
|
+
case path
|
|
106
|
+
when /\.sql$/
|
|
107
|
+
SqlNode.new(unit, phase, path)
|
|
108
|
+
when /\.fox$/
|
|
109
|
+
FoxNode.new(unit, :seed, path)
|
|
110
|
+
else
|
|
111
|
+
raise Error, "Unrecognized file type #{File.basename(path)} in #{unit.dir}"
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
raise Error, "Can't find #{name} in #{dir} from #{unit}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
require 'fcntl'
|
|
2
|
+
|
|
3
|
+
require 'forward_to'
|
|
4
|
+
|
|
5
|
+
include ForwardTo
|
|
6
|
+
|
|
7
|
+
module Command
|
|
8
|
+
class Error < RuntimeError
|
|
9
|
+
attr_reader :cmd
|
|
10
|
+
attr_reader :status
|
|
11
|
+
attr_reader :stdin
|
|
12
|
+
attr_reader :stdout
|
|
13
|
+
attr_reader :stderr
|
|
14
|
+
|
|
15
|
+
def initialize(cmd, status, stdin, stdout, stderr)
|
|
16
|
+
super(stderr.join("\n"))
|
|
17
|
+
@cmd = cmd
|
|
18
|
+
@status = status
|
|
19
|
+
@stdin = stdin
|
|
20
|
+
@stdout = stdout
|
|
21
|
+
@stderr = stderr
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Pipe
|
|
26
|
+
attr_reader :cmd
|
|
27
|
+
attr_reader :status
|
|
28
|
+
|
|
29
|
+
def writer() @pw[1] end
|
|
30
|
+
def reader() @pr[0] end
|
|
31
|
+
def error() @pe[0] end
|
|
32
|
+
|
|
33
|
+
forward_to :reader, :read, :gets
|
|
34
|
+
forward_to :writer, :write, :puts
|
|
35
|
+
|
|
36
|
+
# Remaining output not consumed by #get/#read or through #reader or #error
|
|
37
|
+
attr_reader :stdout
|
|
38
|
+
attr_reader :stdin
|
|
39
|
+
|
|
40
|
+
def initialize(cmd, stderr: nil, fail: true)
|
|
41
|
+
@cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
|
|
42
|
+
@stderr = stderr
|
|
43
|
+
@fail = fail
|
|
44
|
+
|
|
45
|
+
@pw = IO::pipe # pipe[0] for read, pipe[1] for write
|
|
46
|
+
@pr = IO::pipe
|
|
47
|
+
@pe = IO::pipe
|
|
48
|
+
|
|
49
|
+
STDOUT.flush
|
|
50
|
+
|
|
51
|
+
@pid = fork {
|
|
52
|
+
@pw[1].close
|
|
53
|
+
@pr[0].close
|
|
54
|
+
@pe[0].close
|
|
55
|
+
|
|
56
|
+
STDIN.reopen(@pw[0])
|
|
57
|
+
@pw[0].close
|
|
58
|
+
|
|
59
|
+
STDOUT.reopen(@pr[1])
|
|
60
|
+
@pr[1].close
|
|
61
|
+
|
|
62
|
+
STDERR.reopen(@pe[1])
|
|
63
|
+
@pe[1].close
|
|
64
|
+
|
|
65
|
+
exec(cmd)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@pw[0].close
|
|
69
|
+
@pr[1].close
|
|
70
|
+
@pe[1].close
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def wait
|
|
74
|
+
@pw[1].close
|
|
75
|
+
@status = Process.waitpid2(@pid)[1].exitstatus
|
|
76
|
+
|
|
77
|
+
out = @pr[0].readlines.map(&:chomp)
|
|
78
|
+
err = @pe[0].readlines.map(&:chomp)
|
|
79
|
+
|
|
80
|
+
@pr[0].close
|
|
81
|
+
@pe[0].close
|
|
82
|
+
|
|
83
|
+
raise Command::Error.new(@cmd, @status, nil, out, err) if @status != 0 && @fail == true
|
|
84
|
+
|
|
85
|
+
case @stderr
|
|
86
|
+
when true; [out, err]
|
|
87
|
+
when false; out
|
|
88
|
+
when nil
|
|
89
|
+
$stderr.puts err
|
|
90
|
+
out
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Execute the shell command 'cmd' and return standard-output as an array of
|
|
96
|
+
# strings. If :stdin is a string or an array of lines if will be fed to the
|
|
97
|
+
# command on standard-input, if it is a IO object that IO object is piped to
|
|
98
|
+
# the command
|
|
99
|
+
#
|
|
100
|
+
# By default #command pass through standard-error but if :stderr is true,
|
|
101
|
+
# #command will return a tuple of standard-output, standard-error lines. If
|
|
102
|
+
# :stderr is false, standard-error is ignored and is the same as adding
|
|
103
|
+
# "2>/dev/null" to the command
|
|
104
|
+
#
|
|
105
|
+
# #command raises a Command::Error exception if the command return with an
|
|
106
|
+
# exit code != 0 unless :fail is false. In that case the the exit code can be
|
|
107
|
+
# fetched from Command::status
|
|
108
|
+
#
|
|
109
|
+
def command(cmd, stdin: nil, stderr: nil, fail: true)
|
|
110
|
+
cmd = "set -o errexit\nset -o pipefail\n#{cmd}"
|
|
111
|
+
|
|
112
|
+
pw = IO::pipe # pipe[0] for read, pipe[1] for write
|
|
113
|
+
pr = IO::pipe
|
|
114
|
+
pe = IO::pipe
|
|
115
|
+
|
|
116
|
+
STDOUT.flush
|
|
117
|
+
|
|
118
|
+
pid = fork {
|
|
119
|
+
pw[1].close
|
|
120
|
+
pr[0].close
|
|
121
|
+
pe[0].close
|
|
122
|
+
|
|
123
|
+
STDIN.reopen(pw[0])
|
|
124
|
+
pw[0].close
|
|
125
|
+
|
|
126
|
+
STDOUT.reopen(pr[1])
|
|
127
|
+
pr[1].close
|
|
128
|
+
|
|
129
|
+
STDERR.reopen(pe[1])
|
|
130
|
+
pe[1].close
|
|
131
|
+
|
|
132
|
+
exec(cmd)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pw[0].close
|
|
136
|
+
pr[1].close
|
|
137
|
+
pe[1].close
|
|
138
|
+
|
|
139
|
+
if stdin
|
|
140
|
+
case stdin
|
|
141
|
+
when IO; pw[1].write(stdin.read)
|
|
142
|
+
when String; pw[1].write(stdin)
|
|
143
|
+
when Array; pw[1].write(stdin.join("\n") + "\n")
|
|
144
|
+
end
|
|
145
|
+
pw[1].flush
|
|
146
|
+
pw[1].close
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
@status = Process.waitpid2(pid)[1].exitstatus
|
|
150
|
+
|
|
151
|
+
out = pr[0].readlines.map(&:chomp)
|
|
152
|
+
err = pe[0].readlines.map(&:chomp)
|
|
153
|
+
|
|
154
|
+
pw[1].close if !stdin
|
|
155
|
+
pr[0].close
|
|
156
|
+
pe[0].close
|
|
157
|
+
|
|
158
|
+
if @status != 0
|
|
159
|
+
@exception = Command::Error.new(cmd, @status, stdin, out, err)
|
|
160
|
+
raise @exception if fail
|
|
161
|
+
else
|
|
162
|
+
@exception = nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
case stderr
|
|
166
|
+
when true; [out, err]
|
|
167
|
+
when false; out
|
|
168
|
+
when nil
|
|
169
|
+
$stderr.puts err
|
|
170
|
+
out
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Exit status of the last command
|
|
175
|
+
def status() @status end
|
|
176
|
+
|
|
177
|
+
# Stored exception when #command is called with :fail true
|
|
178
|
+
def exception() @exception end
|
|
179
|
+
|
|
180
|
+
# Like command but returns true if the command exited with the expected
|
|
181
|
+
# status. Note that it suppresses standard-error by default
|
|
182
|
+
#
|
|
183
|
+
def command?(cmd, expect: 0, stdin: nil, stderr: false)
|
|
184
|
+
command(cmd, stdin: stdin, stderr: stderr, fail: false)
|
|
185
|
+
@status == expect
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
module_function :command
|
|
189
|
+
module_function :status
|
|
190
|
+
module_function :exception
|
|
191
|
+
module_function :command?
|
|
192
|
+
end
|
|
193
|
+
|