Repository navigation
Expand file tree
/
Copy pathflatten.rb
More file actions
130 lines (121 loc) · 3.87 KB
/
Copy pathflatten.rb
File metadata and controls
130 lines (121 loc) · 3.87 KB
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
# spinel-flatten: inline a require_relative graph into one self-contained file,
# so spinel-reduce and bug reports operate on a single input. Resolves the same
# require_relative paths spinel does; cycle-safe; records each region's origin.
# An unresolvable require_relative aborts with exit 2 (every miss listed on
# stderr first) -- the output would not be the self-contained program.
#
# Usage: spinel-flatten [-o OUT] app.rb
require_relative "tool_common"
# Extract the path from a `require_relative "x"` / `require_relative 'x'` line,
# or "" if the line is not a require_relative.
def require_target(line)
s = line.strip
return "" if !s.start_with?("require_relative")
q = "\""
i = s.index(q)
if !i
q = "'"
i = s.index(q)
end
return "" if !i
rest = s[i + 1, s.length - i - 1]
j = rest.index(q)
return "" if !j
rest[0, j]
end
# Collapse "." and ".." segments so the same file always resolves to the same
# string. Without this, a file reached via two non-canonical paths (e.g.
# `a/../b.rb` and `b.rb`) compares unequal in `seen` and is inlined twice; with
# a require cycle that compounds into runaway re-inlining (the path keeps
# growing) and a flattened file many times larger than the program.
def canonicalize(path)
# An absolute path's leading "/" splits off as an empty segment; remember it
# and put it back, or an absolute entry resolves to a relative path that
# File.exist? rejects. "/.." stays the root, so ".." never stacks when abs.
abs = path.start_with?("/")
stack = []
path.split("/").each { |p|
if p == "" || p == "."
next
elsif p == ".."
if stack.length > 0 && stack[stack.length - 1] != ".."
stack.pop
elsif !abs
stack.push(p)
end
else
stack.push(p)
end
}
abs ? "/" + stack.join("/") : stack.join("/")
end
# Resolve a require_relative target against the requiring file's directory,
# adding ".rb" when absent. Canonicalize before appending the extension so a
# bare ".." (the dir-as-file convention the compiler accepts) resolves to
# "<parent>.rb", not "<dir>/...rb".
def resolve(from_file, target)
base = canonicalize(dir_name(from_file) + "/" + target)
base = base + ".rb" if !base.end_with?(".rb")
base
end
# Count of require_relative targets that did not resolve to a file. Reported
# per miss on stderr as they are found (so every miss is listed, not just the
# first); main aborts without writing when any occurred -- a flattened file
# with a require replaced by a comment silently does not run.
$missing = 0
def inline(real, from, out, seen)
seen.each { |p| return if p == real }
seen.push(real)
if !File.exist?(real)
$stderr.puts "spinel-flatten: missing file: " + real + " (required from " + from + ")"
$missing += 1
return
end
out.push("# >>> " + real)
text = File.read(real)
text.split("\n").each { |line|
t = require_target(line)
if t.length > 0
inline(resolve(real, t), real, out, seen)
else
out.push(line)
end
}
out.push("# <<< " + real)
end
def main
src = ""
outfile = ""
i = 0
while i < ARGV.length
a = ARGV[i]
if a == "-o"
i = i + 1
outfile = ARGV[i] if i < ARGV.length
elsif a == "-h" || a == "--help"
puts "usage: spinel-flatten [-o OUT] app.rb"
puts " inline the require_relative graph of app.rb into one file"
exit(0)
else
src = a
end
i = i + 1
end
die("usage: spinel-flatten [-o OUT] app.rb", 2) if src.length == 0
die("spinel-flatten: no such file: " + src, 2) if !File.exist?(src)
out = []
seen = []
inline(src, src, out, seen)
if $missing > 0
die("spinel-flatten: aborted: " + $missing.to_s + " unresolved require_relative target(s), nothing written", 2)
end
text = out.join("\n") + "\n"
if outfile.length > 0
File.write(outfile, text)
$stderr.puts "spinel-flatten: wrote " + outfile
else
print text
end
exit(0)
end
main