logica_compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +138 -0
- data/Rakefile +76 -0
- data/exe/logica_compiler +7 -0
- data/lib/logica_compiler/active_record/runner.rb +89 -0
- data/lib/logica_compiler/cli.rb +76 -0
- data/lib/logica_compiler/commands/base.rb +45 -0
- data/lib/logica_compiler/commands/clean.rb +25 -0
- data/lib/logica_compiler/commands/compile.rb +57 -0
- data/lib/logica_compiler/commands/install.rb +21 -0
- data/lib/logica_compiler/commands/logica.rb +61 -0
- data/lib/logica_compiler/commands/version.rb +14 -0
- data/lib/logica_compiler/commands/watch.rb +23 -0
- data/lib/logica_compiler/compiler.rb +304 -0
- data/lib/logica_compiler/config.rb +99 -0
- data/lib/logica_compiler/deps/clock.rb +10 -0
- data/lib/logica_compiler/deps/env.rb +35 -0
- data/lib/logica_compiler/deps/shell.rb +31 -0
- data/lib/logica_compiler/deps/sleeper.rb +11 -0
- data/lib/logica_compiler/errors.rb +22 -0
- data/lib/logica_compiler/installer.rb +204 -0
- data/lib/logica_compiler/manifest.rb +31 -0
- data/lib/logica_compiler/railtie.rb +11 -0
- data/lib/logica_compiler/registry.rb +97 -0
- data/lib/logica_compiler/sql_dialect/postgres.rb +13 -0
- data/lib/logica_compiler/sql_dialect/sqlite.rb +13 -0
- data/lib/logica_compiler/sql_safety.rb +176 -0
- data/lib/logica_compiler/tasks/logica_compiler.rake +20 -0
- data/lib/logica_compiler/templates/bin_logica.erb +6 -0
- data/lib/logica_compiler/templates/config.yml.erb +7 -0
- data/lib/logica_compiler/templates/gitignore_block.erb +3 -0
- data/lib/logica_compiler/templates/hello_world.l.erb +9 -0
- data/lib/logica_compiler/templates/initializer.rb.erb +28 -0
- data/lib/logica_compiler/templates/requirements.txt.erb +1 -0
- data/lib/logica_compiler/util.rb +51 -0
- data/lib/logica_compiler/version.rb +5 -0
- data/lib/logica_compiler/watcher.rb +83 -0
- data/lib/logica_compiler.rb +13 -0
- data/sig/logica_compiler.rbs +4 -0
- metadata +177 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "time"
|
|
8
|
+
require "timeout"
|
|
9
|
+
|
|
10
|
+
require_relative "sql_safety"
|
|
11
|
+
require_relative "util"
|
|
12
|
+
|
|
13
|
+
module LogicaCompiler
|
|
14
|
+
class Compiler
|
|
15
|
+
DEFAULT_TIMEOUT = 30
|
|
16
|
+
|
|
17
|
+
# User-friendly aliases → Logica CLI engine names.
|
|
18
|
+
# Python Logica uses "psql" as the PostgreSQL engine name (not "postgres").
|
|
19
|
+
ENGINE_ALIASES = {
|
|
20
|
+
"postgres" => "psql",
|
|
21
|
+
"postgresql" => "psql",
|
|
22
|
+
"pg" => "psql",
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def initialize(config:)
|
|
26
|
+
@config = config
|
|
27
|
+
@logica_version = nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def compile_all!(force: false, prune: true)
|
|
31
|
+
version = logica_version
|
|
32
|
+
out_dir = @config.output_dir_path
|
|
33
|
+
FileUtils.mkdir_p(out_dir)
|
|
34
|
+
|
|
35
|
+
entries = {}
|
|
36
|
+
@config.queries.each do |name, query|
|
|
37
|
+
entries[name.to_s] = compile_one!(name:, query:, force:, prune:, logica_version: version)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
manifest = Manifest.build(
|
|
41
|
+
engine: @config.engine,
|
|
42
|
+
logica_version: version,
|
|
43
|
+
queries: entries
|
|
44
|
+
)
|
|
45
|
+
Manifest.write!(@config.manifest_path, manifest)
|
|
46
|
+
manifest
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def compile_one!(name:, query:, force: false, prune: true, logica_version: self.logica_version)
|
|
50
|
+
out_dir = @config.output_dir_path
|
|
51
|
+
FileUtils.mkdir_p(out_dir)
|
|
52
|
+
|
|
53
|
+
program_path = @config.absolute_path(query.program)
|
|
54
|
+
program_source = File.read(program_path)
|
|
55
|
+
validate_program_engine!(program_source)
|
|
56
|
+
|
|
57
|
+
digest_hex = compute_digest(program_source:, predicate: query.predicate, engine: @config.engine, logica_version:)
|
|
58
|
+
digest = "sha256:#{digest_hex}"
|
|
59
|
+
|
|
60
|
+
sql_filename = "#{name}-#{digest_hex}.sql"
|
|
61
|
+
meta_filename = "#{name}-#{digest_hex}.meta.json"
|
|
62
|
+
sql_path = out_dir.join(sql_filename)
|
|
63
|
+
meta_path = out_dir.join(meta_filename)
|
|
64
|
+
|
|
65
|
+
return existing_entry_for(name:) if !force && entry_exists?(name:, digest_hex:)
|
|
66
|
+
|
|
67
|
+
prune_old_artifacts!(name:, keep_digest_hex: digest_hex) if prune
|
|
68
|
+
|
|
69
|
+
compiled_sql = compile_via_cli!(program_source:, predicate: query.predicate, timeout: compile_timeout)
|
|
70
|
+
compiled_sql = normalize_sql(compiled_sql)
|
|
71
|
+
|
|
72
|
+
sql_with_header = add_sql_header(
|
|
73
|
+
sql: compiled_sql,
|
|
74
|
+
name: name.to_s,
|
|
75
|
+
program: query.program,
|
|
76
|
+
predicate: query.predicate,
|
|
77
|
+
engine: @config.engine,
|
|
78
|
+
digest:
|
|
79
|
+
)
|
|
80
|
+
SqlSafety.validate!(sql_with_header)
|
|
81
|
+
|
|
82
|
+
File.write(sql_path, sql_with_header)
|
|
83
|
+
|
|
84
|
+
meta = {
|
|
85
|
+
name: name.to_s,
|
|
86
|
+
program: query.program,
|
|
87
|
+
predicate: query.predicate,
|
|
88
|
+
engine: @config.engine,
|
|
89
|
+
compiled_at: Time.now.utc.iso8601,
|
|
90
|
+
compiler: { bin: @config.logica_bin, version: logica_version },
|
|
91
|
+
digest: digest,
|
|
92
|
+
}
|
|
93
|
+
File.write(meta_path, "#{JSON.pretty_generate(meta)}\n")
|
|
94
|
+
|
|
95
|
+
{ "digest" => digest, "sql" => sql_filename, "meta" => meta_filename }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def logica_version
|
|
99
|
+
return @logica_version if @logica_version
|
|
100
|
+
|
|
101
|
+
pinned = @config.pinned_logica_version
|
|
102
|
+
return (@logica_version = pinned) if pinned && !pinned.to_s.strip.empty?
|
|
103
|
+
|
|
104
|
+
# If LOGICA_BIN points to a venv-installed executable, infer the venv python and ask pip metadata.
|
|
105
|
+
bin = @config.logica_bin.to_s
|
|
106
|
+
if Util.path_like?(bin)
|
|
107
|
+
venv_dir = File.expand_path("..", File.dirname(bin))
|
|
108
|
+
python = File.join(venv_dir, Util.venv_bin_dir, "python")
|
|
109
|
+
if File.executable?(python)
|
|
110
|
+
stdout, _stderr, status =
|
|
111
|
+
run_cmd_with_timeout!(
|
|
112
|
+
[python, "-c", "import importlib.metadata as m; print(m.version('logica'))"],
|
|
113
|
+
stdin_data: nil,
|
|
114
|
+
timeout: 5
|
|
115
|
+
)
|
|
116
|
+
version = stdout.to_s.strip
|
|
117
|
+
return (@logica_version = version) if status.success? && !version.empty?
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
@logica_version = "unknown"
|
|
122
|
+
rescue StandardError
|
|
123
|
+
@logica_version = @config.pinned_logica_version || "unknown"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def compile_timeout
|
|
129
|
+
Integer(ENV.fetch("LOGICA_COMPILE_TIMEOUT", DEFAULT_TIMEOUT))
|
|
130
|
+
rescue ArgumentError
|
|
131
|
+
DEFAULT_TIMEOUT
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def entry_exists?(name:, digest_hex:)
|
|
135
|
+
entry = existing_entry_for(name:)
|
|
136
|
+
return false unless entry
|
|
137
|
+
return false unless entry["digest"].to_s == "sha256:#{digest_hex}"
|
|
138
|
+
|
|
139
|
+
out_dir = @config.output_dir_path
|
|
140
|
+
sql_file = entry["sql"].to_s
|
|
141
|
+
meta_file = entry["meta"].to_s
|
|
142
|
+
return false if sql_file.empty? || meta_file.empty?
|
|
143
|
+
|
|
144
|
+
out_dir.join(sql_file).exist? && out_dir.join(meta_file).exist?
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def existing_entry_for(name:)
|
|
148
|
+
manifest = Manifest.load(@config.manifest_path)
|
|
149
|
+
manifest.fetch("queries", {}).fetch(name.to_s)
|
|
150
|
+
rescue Errno::ENOENT, KeyError, JSON::ParserError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def compute_digest(program_source:, predicate:, engine:, logica_version:)
|
|
155
|
+
Digest::SHA256.hexdigest([program_source, predicate, engine, logica_version].join("\n"))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def normalize_sql(sql)
|
|
159
|
+
sql = sql.to_s
|
|
160
|
+
sql = extract_main_query_from_psql_script(sql) if logica_engine == "psql"
|
|
161
|
+
sql.strip.sub(/;\s*\z/, "")
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def validate_program_engine!(program_source)
|
|
165
|
+
declared = program_source.match(/@Engine\(\s*["']([^"']+)["']\s*\)\s*;/i)&.captures&.first
|
|
166
|
+
return if declared.nil?
|
|
167
|
+
|
|
168
|
+
return if canonical_engine(declared) == logica_engine
|
|
169
|
+
|
|
170
|
+
raise ArgumentError, "Program @Engine(#{declared.inspect}) does not match config engine #{@config.engine.inspect}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def compile_via_cli!(program_source:, predicate:, timeout:)
|
|
174
|
+
effective_source = ensure_engine_directive(program_source)
|
|
175
|
+
cmd = [@config.logica_bin, "-", "print", predicate.to_s]
|
|
176
|
+
stdout, stderr, status = run_cmd_with_timeout!(cmd, stdin_data: effective_source, timeout:)
|
|
177
|
+
raise CompileError, "Logica compile failed: #{stderr}" unless status.success?
|
|
178
|
+
|
|
179
|
+
stdout
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def ensure_engine_directive(program_source)
|
|
183
|
+
engine = logica_engine
|
|
184
|
+
|
|
185
|
+
# If present, normalize to the canonical Logica engine name (e.g., postgres -> psql).
|
|
186
|
+
if program_source.match?(/@Engine\(\s*["'][^"']+["']\s*\)\s*;/i)
|
|
187
|
+
return program_source.sub(/@Engine\(\s*(["'])([^"']+)\1\s*\)\s*;/i) do
|
|
188
|
+
quote = Regexp.last_match(1)
|
|
189
|
+
"@Engine(#{quote}#{engine}#{quote});"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
"@Engine(\"#{engine}\");\n#{program_source}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def canonical_engine(engine)
|
|
197
|
+
value = engine.to_s.strip
|
|
198
|
+
return "" if value.empty?
|
|
199
|
+
|
|
200
|
+
down = value.downcase
|
|
201
|
+
ENGINE_ALIASES.fetch(down, down)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def logica_engine
|
|
205
|
+
canonical_engine(@config.engine)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def extract_main_query_from_psql_script(sql)
|
|
209
|
+
# Logica's psql engine prints a multi-statement script (preamble + final query).
|
|
210
|
+
# Our runner expects a single SELECT/WITH statement, so keep only the last statement
|
|
211
|
+
# that starts with SELECT/WITH, ignoring semicolons inside strings/comments/dollar-quotes.
|
|
212
|
+
sanitized = SqlSafety.strip_strings_and_comments(sql)
|
|
213
|
+
|
|
214
|
+
statement_starts = [0]
|
|
215
|
+
sanitized.to_enum(:scan, /;/).each do
|
|
216
|
+
statement_starts << Regexp.last_match.end(0)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
last = nil
|
|
220
|
+
statement_starts.each do |start|
|
|
221
|
+
i = start
|
|
222
|
+
i += 1 while i < sanitized.length && sanitized[i].match?(/\s/)
|
|
223
|
+
next if i >= sanitized.length
|
|
224
|
+
|
|
225
|
+
last = i if sanitized[i..].match?(/\A(?:WITH|SELECT)\b/i)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
last ? sql[last..] : sql
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def add_sql_header(sql:, name:, program:, predicate:, engine:, digest:)
|
|
232
|
+
header = <<~SQL
|
|
233
|
+
-- LogicaCompiler
|
|
234
|
+
-- name: #{name}
|
|
235
|
+
-- program: #{program}
|
|
236
|
+
-- predicate: #{predicate}
|
|
237
|
+
-- engine: #{engine}
|
|
238
|
+
-- digest: #{digest}
|
|
239
|
+
-- compiled_at: #{Time.now.utc.iso8601}
|
|
240
|
+
SQL
|
|
241
|
+
|
|
242
|
+
"#{header}\n#{sql.strip}\n"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def prune_old_artifacts!(name:, keep_digest_hex:)
|
|
246
|
+
out_dir = @config.output_dir_path
|
|
247
|
+
|
|
248
|
+
Dir.glob(out_dir.join("#{name}-*.sql").to_s).each do |path|
|
|
249
|
+
next if path.end_with?("-#{keep_digest_hex}.sql")
|
|
250
|
+
|
|
251
|
+
FileUtils.rm_f(path)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
Dir.glob(out_dir.join("#{name}-*.meta.json").to_s).each do |path|
|
|
255
|
+
next if path.end_with?("-#{keep_digest_hex}.meta.json")
|
|
256
|
+
|
|
257
|
+
FileUtils.rm_f(path)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def run_cmd_with_timeout!(cmd, stdin_data:, timeout:)
|
|
262
|
+
stdout_str = +""
|
|
263
|
+
stderr_str = +""
|
|
264
|
+
|
|
265
|
+
Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thr|
|
|
266
|
+
stdin.binmode
|
|
267
|
+
stdout.binmode
|
|
268
|
+
stderr.binmode
|
|
269
|
+
|
|
270
|
+
stdin.write(stdin_data) if stdin_data
|
|
271
|
+
stdin.close
|
|
272
|
+
|
|
273
|
+
out_reader = Thread.new { stdout_str << stdout.read.to_s }
|
|
274
|
+
err_reader = Thread.new { stderr_str << stderr.read.to_s }
|
|
275
|
+
|
|
276
|
+
status = nil
|
|
277
|
+
begin
|
|
278
|
+
status = Timeout.timeout(timeout) { wait_thr.value }
|
|
279
|
+
rescue Timeout::Error
|
|
280
|
+
terminate_process(wait_thr.pid)
|
|
281
|
+
raise
|
|
282
|
+
ensure
|
|
283
|
+
out_reader.join
|
|
284
|
+
err_reader.join
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
[stdout_str, stderr_str, status]
|
|
288
|
+
end
|
|
289
|
+
rescue Timeout::Error
|
|
290
|
+
raise Timeout::Error, "Command timed out after #{timeout}s: #{cmd.join(" ")}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def terminate_process(pid)
|
|
294
|
+
Process.kill("TERM", pid)
|
|
295
|
+
Timeout.timeout(2) { Process.wait(pid) }
|
|
296
|
+
rescue Errno::ESRCH, Errno::ECHILD, Timeout::Error
|
|
297
|
+
begin
|
|
298
|
+
Process.kill("KILL", pid)
|
|
299
|
+
rescue Errno::ESRCH
|
|
300
|
+
nil
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
require_relative "util"
|
|
7
|
+
|
|
8
|
+
module LogicaCompiler
|
|
9
|
+
class Config
|
|
10
|
+
Query = Struct.new(:program, :predicate, keyword_init: true) do
|
|
11
|
+
def initialize(program:, predicate:)
|
|
12
|
+
super(program: program.to_s, predicate: predicate.to_s)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
DEFAULT_ENGINE = "postgres"
|
|
17
|
+
DEFAULT_OUTPUT_DIR = "logica/compiled"
|
|
18
|
+
|
|
19
|
+
attr_reader :config_path, :root, :engine, :output_dir, :queries, :logica_bin
|
|
20
|
+
|
|
21
|
+
def self.load!(config_path = "logica/config.yml", root: default_root)
|
|
22
|
+
new(config_path:, root:).tap(&:load!)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.default_root
|
|
26
|
+
if defined?(Rails) && Rails.respond_to?(:root)
|
|
27
|
+
Rails.root
|
|
28
|
+
else
|
|
29
|
+
Pathname.new(Dir.pwd)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(config_path:, root:)
|
|
34
|
+
@config_path = config_path.to_s
|
|
35
|
+
@root = Pathname(root)
|
|
36
|
+
@engine = DEFAULT_ENGINE
|
|
37
|
+
@output_dir = DEFAULT_OUTPUT_DIR
|
|
38
|
+
@queries = {}
|
|
39
|
+
@logica_bin = env_logica_bin
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load!
|
|
43
|
+
data = YAML.safe_load(File.read(absolute_path(config_path)), permitted_classes: [], aliases: false) || {}
|
|
44
|
+
|
|
45
|
+
@engine = (data["engine"] || DEFAULT_ENGINE).to_s
|
|
46
|
+
@output_dir = (data["output_dir"] || DEFAULT_OUTPUT_DIR).to_s
|
|
47
|
+
@logica_bin ||= (data["logica_bin"] || default_logica_bin).to_s
|
|
48
|
+
|
|
49
|
+
queries = (data["queries"] || {}).to_h
|
|
50
|
+
@queries = queries.each_with_object({}) do |(name, attrs), h|
|
|
51
|
+
attrs = attrs.to_h
|
|
52
|
+
h[name.to_sym] = Query.new(
|
|
53
|
+
program: attrs.fetch("program"),
|
|
54
|
+
predicate: attrs.fetch("predicate")
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def output_dir_path = root.join(output_dir)
|
|
62
|
+
def manifest_path = output_dir_path.join("manifest.json")
|
|
63
|
+
def requirements_path = root.join("logica/requirements.txt")
|
|
64
|
+
def absolute_path(rel_path) = root.join(rel_path).to_s
|
|
65
|
+
|
|
66
|
+
def pinned_logica_version
|
|
67
|
+
req = requirements_path
|
|
68
|
+
return nil unless req.exist?
|
|
69
|
+
|
|
70
|
+
line = req.read.lines.map(&:strip).find { _1.start_with?("logica==") }
|
|
71
|
+
return nil unless line
|
|
72
|
+
|
|
73
|
+
line.split("==", 2).last&.strip
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def env_logica_bin
|
|
79
|
+
value = ENV["LOGICA_BIN"]
|
|
80
|
+
value = nil if value.to_s.strip.empty?
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def default_logica_bin
|
|
85
|
+
# Default lookup order (when LOGICA_BIN / config.yml logica_bin is not set):
|
|
86
|
+
# 1) Project python venv: .venv/bin/logica (or .venv/Scripts/logica.exe on Windows)
|
|
87
|
+
# 2) System-installed logica on PATH
|
|
88
|
+
# 3) Gem-managed venv: tmp/logica_venv/.../logica (installed via `bin/logica install`)
|
|
89
|
+
project_venv_dir = root.join(".venv", Util.venv_bin_dir)
|
|
90
|
+
if (path = Util.find_executable_in_dir(project_venv_dir, "logica"))
|
|
91
|
+
return path
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return "logica" if Util.which("logica")
|
|
95
|
+
|
|
96
|
+
root.join("tmp/logica_venv", Util.venv_bin_dir, "logica")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogicaCompiler
|
|
4
|
+
module Deps
|
|
5
|
+
class Env
|
|
6
|
+
MISSING = :__missing__
|
|
7
|
+
|
|
8
|
+
def [](key) = ENV[key]
|
|
9
|
+
def fetch(key, *args, &block) = ENV.fetch(key, *args, &block)
|
|
10
|
+
def key?(key) = ENV.key?(key)
|
|
11
|
+
|
|
12
|
+
def set(key, value)
|
|
13
|
+
ENV[key] = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(key)
|
|
17
|
+
ENV.delete(key)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def with_temp(updates)
|
|
21
|
+
previous = {}
|
|
22
|
+
updates.each do |k, v|
|
|
23
|
+
previous[k] = key?(k) ? ENV[k] : MISSING
|
|
24
|
+
v.nil? ? delete(k) : set(k, v)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
yield
|
|
28
|
+
ensure
|
|
29
|
+
previous.each do |k, v|
|
|
30
|
+
v == MISSING ? delete(k) : set(k, v)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module LogicaCompiler
|
|
6
|
+
module Deps
|
|
7
|
+
class Shell
|
|
8
|
+
def system(*args)
|
|
9
|
+
Kernel.system(*args)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def system!(*args)
|
|
13
|
+
ok = system(*args)
|
|
14
|
+
raise CommandFailedError.new("Command failed: #{args.join(" ")}", command: args.join(" ")) unless ok
|
|
15
|
+
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capture3(*args)
|
|
20
|
+
Open3.capture3(*args)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def capture!(*args)
|
|
24
|
+
out, err, status = capture3(*args)
|
|
25
|
+
raise CommandFailedError.new(err.to_s, command: args.join(" ")) unless status.success?
|
|
26
|
+
|
|
27
|
+
out
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LogicaCompiler
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class CommandFailedError < Error
|
|
7
|
+
attr_reader :command
|
|
8
|
+
|
|
9
|
+
def initialize(message = nil, command: nil)
|
|
10
|
+
@command = command
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class ConfigurationError < Error; end
|
|
16
|
+
class UnknownQueryError < ConfigurationError; end
|
|
17
|
+
|
|
18
|
+
class InstallError < Error; end
|
|
19
|
+
class CompileError < Error; end
|
|
20
|
+
class LogicaError < Error; end
|
|
21
|
+
class WatcherError < Error; end
|
|
22
|
+
end
|