-
Notifications
You must be signed in to change notification settings - Fork 4
/
ltx2any.rb
302 lines (251 loc) · 10.6 KB
/
ltx2any.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# frozen_string_literal: true
# Copyright 2010-2018, Raphael Reitzig
# Version 0.9 beta
#
# This file is part of ltx2any.
#
# ltx2any is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ltx2any is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ltx2any. If not, see <http://www.gnu.org/licenses/>.
# Add the base directory to the load path
$LOAD_PATH.unshift(File.expand_path(__dir__)) unless
$LOAD_PATH.include?(__dir__) || $LOAD_PATH.include?(File.expand_path(__dir__))
require 'constants'
# Set process name to something less cumbersome
# $0=NAME
# Load stuff from the standard library
require 'digest'
require 'fileutils'
require 'io/console'
require 'singleton'
require 'yaml'
# Load gems
require 'bundler'
Dir.chdir(BASEDIR)
Bundler.require(:default)
Dir.chdir(WORKDIR)
# Load Managers first so other classes can add their dependencies and hooks
Dir["#{BASEDIR}/#{LIBDIR}/*Manager.rb"].each { |f| require f }
# Set up core parameters
PARAMS = ParameterManager.instance
require 'parameters'
# Load rest of the utility classes
Dir["#{BASEDIR}/#{LIBDIR}/*.rb"].each { |f| require f }
# Initialize CLI output wrapper
OUTPUT = Output.instance
# Freeze parameters
ARGV.freeze
Process.exit if CliHelp.instance.provideHelp(ARGV)
CLEAN = []
CLEANALL = []
begin
# At this point, we are sure we want to compile -- process arguments!
begin
PARAMS.processArgs(ARGV)
rescue ParameterException => e
OUTPUT.separate.msg(*e.message.split("\n"))
Process.exit
end
# Make sure all essential dependencies of core and engine are satisfied
begin
missing = []
(DependencyManager.list(source: :core, relevance: :essential) +
DependencyManager.list(source: [:engine, PARAMS[:engine].to_s], relevance: :essential)).each do |d|
missing.push(d) unless d.available?
end
unless missing.empty? # TODO: enter into log?
OUTPUT.separate.error('Missing dependencies', *missing)
Process.exit
end
end
# Check soft dependencies of core and engine; notify user if necessary
begin
missing = []
(DependencyManager.list(source: :core, relevance: :recommended) +
DependencyManager.list(source: [:engine, PARAMS[:engine].to_s], relevance: :recommended)).each do |d|
missing.push(d) unless d.available?
end
OUTPUT.separate.warn('Missing dependencies', *missing) unless missing.empty? # TODO: enter into log?
end
# Switch working directory to jobfile residence
Dir.chdir(PARAMS[:jobpath])
CLEAN.push(PARAMS[:tmpdir])
# Some files we don't want to listen to
toignore = [(PARAMS[:tmpdir]).to_s,
"#{PARAMS[:user_jobname]}.#{Engine[PARAMS[:engine]].extension}",
(PARAMS[:log]).to_s,
"#{PARAMS[:user_jobname]}.err"] + PARAMS[:ignore].split(':')
begin
FileListener.instance.start(PARAMS[:user_jobname], toignore) if PARAMS[:daemon]
rescue MissingDependencyError => e
OUTPUT.warn(e.message)
PARAMS[:daemon] = false
end
# daemon loop
loop do
# inner block that can be cancelled by user
begin
# Reset
# @type [Engine] engine The engine we're running
# @type [Log] log The main log
# @type [Time] start_time The time at which the current run started
engine = Engine[PARAMS[:engine]].new
log = Log.new
log.level = PARAMS[:loglevel]
start_time = Time.now
OUTPUT.start('Copying files to tmp')
# Copy all files to tmp directory (some LaTeX packages fail to work with
# output dir) excepting those we ignore anyways.
# Oh, and don't recurse outside the main directory, duh.
ignore = FileListener.instance.ignored + toignore
exceptions = ignore + ignore.map { |s| "./#{s}" } +
Dir['.*'] + Dir['./.*'] # drop hidden files, in p. . and ..
define_singleton_method(:copy2tmp) do |files|
files.each do |f|
if File.symlink?(f)
# Avoid trouble with symlink loops
# Delete old symlink if there is one, because:
# If a proper file or directory has been replaced with a symlink,
# remove the obsolete stuff.
# If there already is a symlink, delete because it might have been
# relinked.
FileUtils.rm_f("#{PARAMS[:tmpdir]}/#{f}")
# Create new symlink instead of copying
File.symlink("#{PARAMS[:jobpath]}/#{f}", "#{PARAMS[:tmpdir]}/#{f}")
elsif File.directory?(f)
FileUtils.mkdir_p("#{PARAMS[:tmpdir]}/#{f}")
copy2tmp(Dir.children(f).map { |s| "#{f}/#{s}" })
# TODO: Is this necessary? Why not just copy? (For now, safer and more adaptable.)
else
FileUtils.cp(f, "#{PARAMS[:tmpdir]}/#{f}")
end
end
end
# tmp dir may have been removed (either by DaemonPrompt or the outside)
if !File.exist?(PARAMS[:tmpdir])
FileUtils.mkdir_p(PARAMS[:tmpdir])
elsif !File.directory?(PARAMS[:tmpdir])
OUTPUT.message("File #{PARAMS[:tmpdir]} exists but is not a directory")
Process.exit
end
# (Re-)Copy content to tmp
copy2tmp(Dir.children('.').reject { |f| exceptions.include?(f) })
OUTPUT.stop(:success)
# Move into temporary directory
Dir.chdir(PARAMS[:tmpdir])
# Delete former results in order not to pretend success
FileUtils.rm_f("#{PARAMS[:jobname]}.#{engine.extension}")
# Read hashes
HashManager.instance.from_file(HASHFILE.to_s)
# Run extensions that may need to do something before the engine
Extension.run_all(:before, OUTPUT, log)
# Run engine as often as specified
run = 1
result = []
loop do
# Run engine
OUTPUT.start("#{engine.name}(#{run}) running")
result = engine.exec
OUTPUT.stop(result[:success] ? :success : :error)
break unless File.exist?("#{PARAMS[:jobname]}.#{engine.extension}")
# Run extensions that need to do something after this iteration
Extension.run_all(run, OUTPUT, log)
run += 1
break if ((PARAMS[:engineruns]).positive? && run > PARAMS[:engineruns]) || # User set number of runs
(PARAMS[:engineruns] <= 0 && !engine.do?) # User set automatic mode
end
# Save log messages of last engine run
log.add_messages(engine.name, :engine, result[:messages], result[:log])
# Run extensions that may need to do something after all engine runs
Extension.run_all(:after, OUTPUT, log)
# Give error/warning counts to user
errorS = log.count(:error) == 1 ? '' : 's'
warningS = log.count(:warning) == 1 ? '' : 's'
OUTPUT.msg("There were #{log.count(:error)} error#{errorS} " \
"and #{log.count(:warning)} warning#{warningS}.")
# Pick up output if present
if File.exist?("#{PARAMS[:jobname]}.#{engine.extension}")
FileUtils.cp("#{PARAMS[:jobname]}.#{engine.extension}",
"#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.#{engine.extension}")
OUTPUT.msg("Output generated at #{PARAMS[:user_jobname]}.#{engine.extension}")
else
OUTPUT.msg('No output generated, probably due to fatal errors.')
end
# Write log
unless log.empty?
OUTPUT.start('Assembling log files')
# Manage messages from extensions
Extension.list.each do |ext|
if !log.has_messages?(ext.name) && File.exist?(".#{NAME}_extensionmsg_#{ext.name}")
# Extension did not run but has run before; load messages from then!
old = File.open(".#{NAME}_extensionmsg_#{ext.name}", 'r') do |f|
f.readlines.join
end
old = YAML.load(old)
log.add_messages(ext.name, old[0], old[1], old[2])
elsif log.has_messages?(ext.name)
# Write new messages
File.write(".#{NAME}_extensionmsg_#{ext.name}", YAML.dump(log.messages(ext.name)))
end
end
log.finish
LogWriter[:raw].write(log)
logfile = LogWriter[PARAMS[:logformat]].write(log, PARAMS[:loglevel])
FileUtils.cp(logfile, "#{PARAMS[:jobpath]}/#{logfile}")
OUTPUT.stop(:success)
OUTPUT.msg("Log file generated at #{logfile}")
CLEANALL.push("#{PARAMS[:jobpath]}/#{logfile}")
CLEANALL.uniq!
runtime = Time.now - start_time
# Don't show runtimes of less than 5s (arbitrary)
if runtime / 60 >= 1 || runtime % 60 >= 5
OUTPUT.msg("Took #{format('%d min ', runtime / 60)} #{format('%d sec', runtime % 60)}")
end
end
rescue Interrupt, SystemExit # User cancelled current run
OUTPUT.stop(:cancel)
rescue Exception => e
OUTPUT.stop(:error)
raise e
ensure
# Return from temporary directory
Dir.chdir(PARAMS[:jobpath])
end
FileListener.instance.waitForChanges(OUTPUT) if PARAMS[:daemon] && (PARAMS[:listeninterval]).positive?
# Rerun!
OUTPUT.separate
break unless PARAMS[:daemon]
end
rescue Interrupt, SystemExit
OUTPUT.separate.msg('Shutdown')
rescue Exception => e
raise e if PARAMS[:user_jobname].nil?
OUTPUT.separate.error(e.message, "See #{PARAMS[:user_jobname]}.err for details.")
File.write("#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.err", "#{e.inspect}\n\n#{e.backtrace.join("\n")}")
CLEANALL.push("#{PARAMS[:jobpath]}/#{PARAMS[:user_jobname]}.err")
# This is reached due to programming errors or if ltx2any quits early,
# i.e. if no feasible input file has been specified.
# Neither case warrants special action.
# For debugging purposes, reraise so we don't die silently.
# Exit immediately. Don't clean up, logs may be necessary for debugging.
# Kernel.exit!(FALSE) # Leads to inconsistent behaviour regarding -c/-ca
end
# Write current hashes
HashManager.instance.to_file("#{PARAMS[:tmpdir]}/#{HASHFILE}") if !PARAMS[:clean] && !HashManager.instance.empty?
# NOTE: old version stored hashes for *all* files. Now we only store such
# that were needed earlier. Is that sufficient?
# Stop file listeners
FileListener.instance.stop if PARAMS[:daemon] && FileListener.instance.runs?
# Remove temps if so desired.
CLEAN.each { |f| FileUtils.rm_rf(f) } if PARAMS[:clean]
CLEANALL.each { |f| FileUtils.rm_rf(f) } if PARAMS[:cleanall]