-
-
Notifications
You must be signed in to change notification settings - Fork 283
/
brew_dumper.rb
235 lines (202 loc) · 7.19 KB
/
brew_dumper.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
# frozen_string_literal: true
require "json"
require "tsort"
module Bundle
# TODO: refactor into multiple modules
module BrewDumper
module_function
def reset!
Bundle::BrewServices.reset!
@formulae = nil
@formulae_by_full_name = nil
@formulae_by_name = nil
@formula_aliases = nil
@formula_oldnames = nil
end
def formulae
return @formulae if @formulae
formulae_by_full_name
@formulae
end
def formulae_by_full_name(name = nil)
return @formulae_by_full_name[name] if !name.nil? && @formulae_by_full_name&.key?(name)
require "formula"
require "formulary"
Formulary.enable_factory_cache!
@formulae_by_name ||= {}
@formulae_by_full_name ||= {}
if name.nil?
formulae = Formula.installed.map(&method(:add_formula))
sort!(formulae)
return @formulae_by_full_name
end
formula = Formula[name]
add_formula(formula)
rescue FormulaUnavailableError => e
opoo "'#{name}' formula is unreadable: #{e}"
{}
end
def formulae_by_name(name)
formulae_by_full_name(name) || @formulae_by_name[name]
end
def dump(describe: false, no_restart: false)
requested_formula = formulae.select do |f|
f[:installed_on_request?] || !f[:installed_as_dependency?]
end
requested_formula.map do |f|
brewline = if describe && f[:desc].present?
f[:desc].split("\n").map { |s| "# #{s}\n" }.join
else
""
end
brewline += "brew \"#{f[:full_name]}\""
args = f[:args].map { |arg| "\"#{arg}\"" }.sort.join(", ")
brewline += ", args: [#{args}]" unless f[:args].empty?
brewline += ", restart_service: true" if !no_restart && BrewServices.started?(f[:full_name])
brewline += ", link: #{f[:link?]}" unless f[:link?].nil?
brewline
end.join("\n")
end
def formula_aliases
return @formula_aliases if @formula_aliases
@formula_aliases = {}
formulae.each do |f|
aliases = f[:aliases]
next if aliases.blank?
aliases.each do |a|
@formula_aliases[a] = f[:full_name]
if f[:full_name].include? "/" # tap formula
tap_name = f[:full_name].rpartition("/").first
@formula_aliases["#{tap_name}/#{a}"] = f[:full_name]
end
end
end
@formula_aliases
end
def formula_oldnames
return @formula_oldnames if @formula_oldnames
@formula_oldnames = {}
formulae.each do |f|
oldname = f[:oldname]
next if oldname.blank?
@formula_oldnames[oldname] = f[:full_name]
if f[:full_name].include? "/" # tap formula
tap_name = f[:full_name].rpartition("/").first
@formula_oldnames["#{tap_name}/#{oldname}"] = f[:full_name]
end
end
@formula_oldnames
end
def add_formula(formula)
hash = formula_to_hash formula
@formulae_by_name[hash[:name]] = hash
@formulae_by_full_name[hash[:full_name]] = hash
hash
end
private_class_method :add_formula
def formula_to_hash(formula)
keg = if formula.linked?
link = true if formula.keg_only?
formula.linked_keg
else
link = false unless formula.keg_only?
formula.any_installed_prefix
end
if keg
require "tab"
tab = Tab.for_keg(keg)
args = tab.used_options.map(&:name)
version = begin
keg.realpath.basename
rescue
# silently handle broken symlinks
nil
end.to_s
args << "HEAD" if version.start_with?("HEAD")
installed_as_dependency = tab.installed_as_dependency
installed_on_request = tab.installed_on_request
runtime_dependencies = if (runtime_deps = tab.runtime_dependencies)
runtime_deps.map { |d| d["full_name"] }
.compact
end
poured_from_bottle = tab.poured_from_bottle
end
runtime_dependencies ||= formula.runtime_dependencies.map(&:name)
bottled = if (stable = formula.stable) && stable.bottle_defined?
bottle_hash = formula.bottle_hash.deep_symbolize_keys
stable.bottled?
end
{
name: formula.name,
desc: formula.desc,
oldname: formula.oldname,
full_name: formula.full_name,
aliases: formula.aliases,
any_version_installed?: formula.any_version_installed?,
args: Array(args).uniq,
version: version,
installed_as_dependency?: (installed_as_dependency || false),
installed_on_request?: (installed_on_request || false),
dependencies: runtime_dependencies,
build_dependencies: formula.deps.select(&:build?).map(&:name).uniq,
conflicts_with: formula.conflicts.map(&:name),
pinned?: (formula.pinned? || false),
outdated?: (formula.outdated? || false),
link?: link,
poured_from_bottle?: (poured_from_bottle || false),
bottle: (bottle_hash || false),
bottled: (bottled || false),
official_tap: (formula.tap&.official? || false),
}
end
private_class_method :formula_to_hash
class Topo < Hash
include TSort
alias tsort_each_node each_key
def tsort_each_child(node, &block)
fetch(node.downcase).sort.each(&block)
end
end
def sort!(formulae)
# Step 1: Sort by formula full name while putting tap formulae behind core formulae.
# So we can have a nicer output.
formulae = formulae.sort do |a, b|
if a[:full_name].exclude?("/") && b[:full_name].include?("/")
-1
elsif a[:full_name].include?("/") && b[:full_name].exclude?("/")
1
else
a[:full_name] <=> b[:full_name]
end
end
# Step 2: Sort by formula dependency topology.
topo = Topo.new
formulae.each do |f|
topo[f[:name]] = topo[f[:full_name]] = f[:dependencies].map do |dep|
ff = formulae_by_name(dep)
next if ff.blank?
next unless ff[:any_version_installed?]
ff[:full_name]
end.compact
end
@formulae = topo.tsort
.map { |name| @formulae_by_full_name[name] || @formulae_by_name[name] }
.uniq { |f| f[:full_name] }
rescue TSort::Cyclic => e
e.message =~ /\["([^"]*)".*"([^"]*)"\]/
cycle_first = Regexp.last_match(1)
cycle_last = Regexp.last_match(2)
odie e.message if !cycle_first || !cycle_last
odie <<~EOS
Formulae dependency graph sorting failed (likely due to a circular dependency):
#{cycle_first}: #{topo[cycle_first]}
#{cycle_last}: #{topo[cycle_last]}
Please run the following commands and try again:
brew update
brew uninstall --ignore-dependencies --force #{cycle_first} #{cycle_last}
brew install #{cycle_first} #{cycle_last}
EOS
end
private_class_method :sort!
end
end