-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinno_setup.rb
399 lines (359 loc) · 12.5 KB
/
inno_setup.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
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# @BEGIN_LICENSE
#
# Halyard - Multimedia authoring and playback system
# Copyright 1993-2009 Trustees of Dartmouth College
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 2.1 of the
# License, or (at your option) any later version.
#
# This program 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
# USA.
#
# @END_LICENSE
# Map $', etc., to actual names.
require 'english'
require 'pathname'
require 'digest/sha1'
# Interface to InnoSetup +*.iss+ files.
module InnoSetup
# A parsed *.iss file.
class SourceFile
# The base directory for finding source files.
attr_reader :base_dir
# The Components described by this installer.
attr_reader :components
# The FileSets described by this installer.
attr_reader :file_sets
# Read and parse the +*.iss+ file at _path_.
def initialize path, base_dir, defines
@base_dir = base_dir
source = File::open(path, "r") {|f| f.read }
preprocessed = InnoSetup::preprocess(source, defines)
sections = InnoSetup::split_into_sections(preprocessed)
cs = parse_section sections['Components'], Component
@components = build_hash(cs) {|c| c.name }
@file_sets = parse_section sections['Files'], FileSet
end
# Create a +.spec+ file, listing the versions of all of the components,
# along with the build information specified.
def spec_file params
file = ""
params.each do |k,v|
file << "%s: %s\n" % [k, v]
end
file << "\n"
@components.each do |name, component|
next unless component.includes_manifest?
file << component.manifest_meta
file << "\n"
end
file
end
private
# Build a hash table by iterating over a list.
def build_hash items
result = {}
items.each do |v|
k = yield v
if result.has_key? k
raise "Duplicate hash key: #{k} with values #{v} and #{result[k]}"
end
result[k] = v
end
result
end
# Parse _section_ as series of declarations, constructing an
# instance of _klass_ for each.
def parse_section section, klass
result = []
section.each do |line|
next if line =~ /^\s*;/ || line =~ /^\s*$/
result << klass::new(self, InnoSetup::parse_decl_line(line))
end
result
end
end
# A Component is a set of files (and related actions) which can be
# included in an installation.
class Component
# An internal name. Never displayed to the user.
attr_reader :name
# Create a Component from the specified _properties_.
def initialize iss_file, properties
@iss_file = iss_file
@name = properties['Name']
end
# Get all the file sets associated with this component.
def file_sets
@iss_file.file_sets.select {|fs| fs.components.include? name }
end
# For each FileSet in this component, call FileSet#files and merge
# the results.
def files
file_sets.map{|fs| fs.files }.flatten
end
# Compute the manifest of a set of files.
def manifest
return @manifest if @manifest
result = []
app_prefix = /^\{app\}\//
manifest_regexp = /#{manifest_name}$/
files.each do |file|
path, installed_path = file.source, file.dest
next unless installed_path =~ app_prefix
# Skip the MANIFEST file if it already exists. Should only happen
# when doing a dirty build.
# TODO - we should only skip if we're doing a dirty build; if we're
# doing a normal build, and have a preexisting manifest, we should
# fail hard.
next if path =~ manifest_regexp
digest = Digest::SHA1.hexdigest(IO.read(path))
# TODO - Should use a struct, not an array.
result << [digest, File.size(path),
installed_path.gsub(app_prefix, '')]
end
@manifest =
result.sort_by {|x| x[2] }.map {|x| "#{x[0]} #{x[1]} #{x[2]}\n" }.join
end
# The name of the MANIFEST file for this component.
def manifest_name
"MANIFEST.#{name}"
end
# Does this component include a MANIFEST._name_ file? This should
# be a single, non-wildcarded declaration of the form:
#
# Source: MANIFEST.name; DestDir: {app}; \
# Flags: skipifsourcedoesntexist; Components: name
#
# ...where _name_ is the name of this component.
def includes_manifest?
file_sets.any? {|fs| fs.source == manifest_name }
end
def manifest_meta
digest = Digest::SHA1.hexdigest(manifest)
"#{digest} #{manifest.size} #{manifest_name}"
end
end
# A single line from the +[Files]+ section, corresponding to zero or more
# actual files.
class FileSet
# A source specification.
attr_reader :source
# Various flags, represented as an array of strings.
attr_reader :flags
# The directory in which to place these files.
attr_reader :dest_dir
# File patterns to exclude from this file set.
attr_reader :excludes
# The components to which this FileSet belongs.
attr_reader :components
# The source and destination of a single file.
FileCopy = Struct.new(:source, :dest)
# Create a FileSet from the specified _properties_.
def initialize iss_file, properties
@iss_file = iss_file
@source = properties['Source']
@flags = (properties['Flags'] || '').split(' ')
@dest_dir = properties['DestDir']
@excludes = (properties['Excludes'] || '').split(',')
@components = (properties['Components'] || '').split(' ')
end
# Get the source and destination paths for all files in this FileSet.
# The source paths should point to the local filesystem. The
# destination paths may be +nil+ (for files which don't get installed),
# or may begin with a directory pattern such as +{app}+.
def files
src_dir, src_glob = source_dir_and_glob
src_base = cleanpath "#{@iss_file.base_dir}/#{src_dir}"
src_ruby_glob = translate_glob src_glob
files = apply_exclusions(expand_glob_in_dir(src_ruby_glob, src_base))
# Build our result list.
result = []
files.each do |f|
src = "#{src_base}/#{f}"
next if File.directory?(src)
dst = dest_path_for_file f
result.push FileCopy.new(src, dst)
end
# Fail on empty filesets, unless they're allowed.
if result.empty? && !flags.include?('skipifsourcedoesntexist')
raise "Unexpected empty file set: #{source}"
end
result
end
private
# Split our 'Source' into a directory and a glob component.
def source_dir_and_glob
path = fix_path source
dir, glob = File.dirname(path), File.basename(path)
raise "Can't handle ISS pattern #{source}" if dir.include?('*')
return dir, glob
end
# Translate a glob from ISS format to Ruby format.
def translate_glob iss_glob
raise "Can't expand path #{iss_glob}" if iss_glob.include?('**')
prefix = flags.include?('recursesubdirs') ? "**/" : ""
"#{prefix}#{iss_glob}"
end
def expand_glob_in_dir glob, dir
Dir.chdir(dir) { Dir[glob] }
end
# If any path in _paths_ has a sequence of components that matches
# an equivalent sequence of components (with glob expansion) in
# our 'Excludes' list, then remove that name from our list. This
# filters out CVS directories and whatnot.
def apply_exclusions paths
paths.reject do |path|
excludes.any? do |pattern|
globs = pattern.split("\\")
components = path.split("/")
# Inno Setup includes a feature in which you can anchor excludes at
# the root by starting the exclude with a "\". Since I don't want
# to make this more complicated than I have to, I'm not implementing
# this feature at this time.
if globs[0] == ""
raise "Can't handle anchored exclude #{pattern}"
end
globs_match_strings_anywhere? globs, components
end
end
end
# Try to match an array of globs against an array of strings. If
# the corresponding strings match the corresponding globs, return
# true, otherwise strip off the first string and try matching one
# level deeper.
def globs_match_strings_anywhere? globs, strings
while (!globs.empty? && !strings.empty? &&
globs.length <= strings.length)
if globs_match_strings? globs, strings
return true
end
strings.shift
end
return false
end
# Check if each glob in _globs_ expands to the corresponding
# string in _strings_. _strings_ must be at least as long as
# _globs_.
def globs_match_strings? globs, strings
globs.zip(strings).all? do |glob, string|
File.fnmatch(glob, string)
end
end
def cleanpath path
Pathname.new(path).cleanpath.to_s
end
def dest_path_for_file file
if flags.include?('dontcopy')
nil
else
"#{fix_path(dest_dir)}/#{file}"
end
end
def fix_path path
path.gsub /\\/, '/'
end
end
# Preprocess _text_ using the same rules as the InnoSetup preprocessor.
def preprocess(text, defines={})
defines = defines.dup
result = []
active_stack = []
active = true
text.each_line do |line|
case line
when /^#\s*define\s+(\w+)\s+(\w*)\s*$/
defines[$1] = $2 if active
when /^#\s*if\s+(\w+)\s*$/
active_stack.push active
active = active &&
case preprocessor_expand($1, defines)
when '0': false
when '1': true
else raise "Can't parse: #{line}" end
when /^#\s*ifdef\s+(\w+)\s*$/
active_stack.push active
active = active && defines.has_key?($1)
when /^#\s*ifndef\s+(\w+)\s*$/
active_stack.push active
active = active && !defines.has_key?($1)
when /^#\s*if\s+[vV][eE][rR]\s+(<|>)=?\s+0x[0-9a-zA-Z]{8}\s*$/
# Allow version checks, but always consider the version to be higher
# than the version compared to.
active_stack.push active
active = active && ($1 == ">")
when /^#\s*else\s*$/
# If we have a parent if, and it's inactive, we don't actually
# want to do anything here.
active = !active if active_stack.empty? || active_stack.last
when /^#\s*endif(\s+(\w+))?\s*$/
active = active_stack.pop
when /^#\s*error\s+(.+)$/
raise $1 if active
when /^#.*$/
raise "Unknown preprocessor command: #{line}"
else
result << line if active
end
end
raise "Missing #endif" unless active_stack.empty?
result.join
end
# Expand all the preprocessor _definitions_ in _str_.
def preprocessor_expand str, definitions
while definitions.has_key? str
str = definitions[str].to_s
end
str
end
# Split _text_ into sections, split the sections into lines, and
# store the result in a hash table by section name. Sections begin
# with a line of the form '[SectionName]'.
def split_into_sections text
result = {}
current = nil
text.each_line do |line|
line.chomp!
if line =~ /^\[(\w+)\]\s*$/
raise "Duplicate section: #{$1}" if result[$1]
current = []
result[$1] = current
else
current << line if current
end
end
result
end
# Parse a data line. This is trickier than it should be, because Inno
# Setup uses a fairly unpleasant quoting format.
def parse_decl_line line
result = {}
until line.empty?
# Peel a key off our line.
line =~ /^(; )?(\w+): / or raise "Can't parse: #{line}"
key, line = $2, $POSTMATCH
# Peel a value off our line.
case line
when /^([^;"]+)/
result[key], line = $1, $POSTMATCH
when /^"([^"]*(""[^"]*)*)"/
str, line = $1, $POSTMATCH
result[key] = str.gsub(/""/, '"')
else
raise "Can't parse argument: #{line}"
end
end
result
end
module_function :preprocess, :preprocessor_expand, :split_into_sections
module_function :parse_decl_line
end