Skip to content

Commit e7e5dfa

Browse files
committed
Add install steps framework
- Introduce shared structured install step data and execution. - Add lint guardrails before formulae and casks adopt the DSL. - Capture the JSON API postinstall/preflight/postflight plan.
1 parent fe40380 commit e7e5dfa

11 files changed

Lines changed: 1041 additions & 4 deletions

Library/Homebrew/install_steps.rb

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module Homebrew
5+
# Declarative install steps that can be serialised through the JSON APIs.
6+
module InstallSteps
7+
Step = T.type_alias { T::Hash[String, T.untyped] }
8+
Steps = T.type_alias { T::Array[Step] }
9+
10+
class DSL
11+
((instance_methods + private_instance_methods) -
12+
(BasicObject.instance_methods + BasicObject.private_instance_methods) -
13+
[:__callee__, :__method__, :class, :object_id]).each { |method| undef_method method }
14+
15+
sig {
16+
params(
17+
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
18+
default_source_base: ::T.nilable(::T.any(::String, ::Symbol)),
19+
default_target_base: ::T.nilable(::T.any(::String, ::Symbol)),
20+
).void
21+
}
22+
def initialize(default_base: nil, default_source_base: nil, default_target_base: nil)
23+
@default_base = ::T.let(default_base, ::T.nilable(::T.any(::String, ::Symbol)))
24+
@default_source_base = ::T.let(default_source_base, ::T.nilable(::T.any(::String, ::Symbol)))
25+
@default_target_base = ::T.let(default_target_base, ::T.nilable(::T.any(::String, ::Symbol)))
26+
@steps = ::T.let([], Steps)
27+
end
28+
29+
sig { returns(Steps) }
30+
attr_reader :steps
31+
32+
sig {
33+
params(
34+
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
35+
default_source_base: ::T.nilable(::T.any(::String, ::Symbol)),
36+
default_target_base: ::T.nilable(::T.any(::String, ::Symbol)),
37+
block: ::T.nilable(::T.proc.void),
38+
).returns(Steps)
39+
}
40+
def self.build(default_base: nil, default_source_base: nil, default_target_base: nil, &block)
41+
dsl = new(default_base:, default_source_base:, default_target_base:)
42+
dsl.instance_eval(&block) if block
43+
dsl.steps
44+
end
45+
46+
sig { params(steps: ::T::Array[::T.untyped]).returns(Steps) }
47+
def self.normalise_steps(steps)
48+
steps.map do |step|
49+
::T.cast(::Utils.deep_stringify_symbols(step), Step)
50+
end
51+
end
52+
53+
sig { params(path: ::T.any(::String, ::Pathname), base: ::T.nilable(::T.any(::String, ::Symbol))).void }
54+
def mkdir(path, base: nil)
55+
add_step("mkdir", "path" => path_spec(path, base:, default_base: @default_base))
56+
end
57+
58+
alias mkdir_p mkdir
59+
60+
sig { params(path: ::T.any(::String, ::Pathname), base: ::T.nilable(::T.any(::String, ::Symbol))).void }
61+
def touch(path, base: nil)
62+
add_step("touch", "path" => path_spec(path, base:, default_base: @default_base))
63+
end
64+
65+
sig {
66+
params(
67+
source: ::T.any(::String, ::Pathname),
68+
target: ::T.any(::String, ::Pathname),
69+
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
70+
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
71+
force: ::T::Boolean,
72+
).void
73+
}
74+
def move(source, target, source_base: nil, target_base: nil, force: false)
75+
add_step("move",
76+
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
77+
"target" => path_spec(target, base: target_base, default_base: @default_target_base),
78+
"force" => force)
79+
end
80+
81+
alias mv move
82+
83+
sig {
84+
params(
85+
source: ::T.any(::String, ::Pathname),
86+
target: ::T.any(::String, ::Pathname),
87+
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
88+
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
89+
).void
90+
}
91+
def move_children(source, target, source_base: nil, target_base: nil)
92+
add_step("move_children",
93+
"source" => path_spec(source, base: source_base, default_base: @default_source_base),
94+
"target" => path_spec(target, base: target_base, default_base: @default_target_base))
95+
end
96+
97+
sig {
98+
params(
99+
source: ::T.any(::String, ::Pathname),
100+
target: ::T.any(::String, ::Pathname),
101+
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
102+
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
103+
source_formula: ::T.nilable(::String),
104+
target_formula: ::T.nilable(::String),
105+
force: ::T::Boolean,
106+
uninstall: ::T::Boolean,
107+
).void
108+
}
109+
def symlink(source, target, source_base: nil, target_base: nil, source_formula: nil, target_formula: nil,
110+
force: false, uninstall: false)
111+
add_step("symlink",
112+
"source" => path_spec(source, base: source_base, formula: source_formula,
113+
default_base: @default_source_base),
114+
"target" => path_spec(target, base: target_base, formula: target_formula,
115+
default_base: @default_target_base),
116+
"force" => force,
117+
"uninstall" => uninstall)
118+
end
119+
120+
sig {
121+
params(
122+
source: ::T.any(::String, ::Pathname),
123+
target: ::T.any(::String, ::Pathname),
124+
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
125+
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
126+
source_formula: ::T.nilable(::String),
127+
target_formula: ::T.nilable(::String),
128+
force: ::T::Boolean,
129+
uninstall: ::T::Boolean,
130+
).void
131+
}
132+
def ln_s(source, target, source_base: nil, target_base: nil, source_formula: nil, target_formula: nil,
133+
force: false, uninstall: false)
134+
symlink(source, target, source_base:, target_base:, source_formula:, target_formula:, force:, uninstall:)
135+
end
136+
137+
sig {
138+
params(
139+
source: ::T.any(::String, ::Pathname),
140+
target: ::T.any(::String, ::Pathname),
141+
source_base: ::T.nilable(::T.any(::String, ::Symbol)),
142+
target_base: ::T.nilable(::T.any(::String, ::Symbol)),
143+
source_formula: ::T.nilable(::String),
144+
target_formula: ::T.nilable(::String),
145+
uninstall: ::T::Boolean,
146+
).void
147+
}
148+
def ln_sf(source, target, source_base: nil, target_base: nil, source_formula: nil, target_formula: nil,
149+
uninstall: false)
150+
symlink(source, target, source_base:, target_base:, source_formula:, target_formula:, force: true, uninstall:)
151+
end
152+
153+
private
154+
155+
sig { params(type: ::String, fields: ::T.untyped).void }
156+
def add_step(type, **fields)
157+
step = fields.transform_keys(&:to_s)
158+
step["type"] = type
159+
@steps << ::T.cast(::Utils.deep_compact_blank(step), Step)
160+
end
161+
162+
sig {
163+
params(
164+
path: ::T.any(::String, ::Pathname),
165+
base: ::T.nilable(::T.any(::String, ::Symbol)),
166+
formula: ::T.nilable(::String),
167+
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
168+
).returns(Step)
169+
}
170+
def path_spec(path, base:, formula: nil, default_base: nil)
171+
{
172+
"base" => (base || default_base_for(path, default_base))&.to_s,
173+
"formula" => formula,
174+
"path" => path.to_s,
175+
}.compact_blank
176+
end
177+
178+
sig {
179+
params(
180+
path: ::T.any(::String, ::Pathname),
181+
default_base: ::T.nilable(::T.any(::String, ::Symbol)),
182+
).returns(::T.nilable(::T.any(::String, ::Symbol)))
183+
}
184+
def default_base_for(path, default_base)
185+
return if ::Pathname.new(path.to_s).absolute?
186+
187+
default_base
188+
end
189+
end
190+
191+
class Runner
192+
sig { params(context: T.untyped).void }
193+
def initialize(context:)
194+
@context = context
195+
end
196+
197+
sig { params(steps: Steps, phase: Symbol).void }
198+
def run(steps, phase: :install)
199+
DSL.normalise_steps(steps).each do |step|
200+
if phase == :uninstall
201+
run_uninstall_step(step)
202+
else
203+
run_install_step(step)
204+
end
205+
end
206+
end
207+
208+
private
209+
210+
sig { params(step: Step).void }
211+
def run_install_step(step)
212+
case step.fetch("type")
213+
when "mkdir"
214+
resolve_path(step.fetch("path")).mkpath
215+
when "touch"
216+
path = resolve_path(step.fetch("path"))
217+
path.dirname.mkpath
218+
FileUtils.touch path
219+
when "move"
220+
source = resolve_path(step.fetch("source"))
221+
target = resolve_path(step.fetch("target"))
222+
target.dirname.mkpath
223+
FileUtils.mv source, target, force: step["force"] == true
224+
when "move_children"
225+
source = resolve_path(step.fetch("source"))
226+
target = resolve_path(step.fetch("target"))
227+
target.mkpath
228+
children = source.children.reject { |child| child == target }
229+
return if children.empty?
230+
231+
FileUtils.mv children, target
232+
when "symlink"
233+
target = resolve_path(step.fetch("target"))
234+
target.dirname.mkpath
235+
FileUtils.rm_f target if step["force"] == true
236+
File.symlink link_source(step.fetch("source")), target
237+
else
238+
raise ArgumentError, "unknown install step: #{step.fetch("type")}"
239+
end
240+
end
241+
242+
sig { params(step: Step).void }
243+
def run_uninstall_step(step)
244+
return if step.fetch("type") != "symlink"
245+
return if step["uninstall"] != true
246+
247+
target = resolve_path(step.fetch("target"))
248+
FileUtils.rm_f target if target.symlink?
249+
end
250+
251+
sig { params(spec: T.untyped).returns(Pathname) }
252+
def resolve_path(spec)
253+
path_spec = normalise_path_spec(spec)
254+
path = Pathname(path_spec.fetch("path"))
255+
base = path_spec["base"]
256+
257+
return path.expand_path if base.blank? || base == "absolute"
258+
return path if base == "relative"
259+
260+
root_path(base, path_spec["formula"])/path
261+
end
262+
263+
sig { params(spec: T.untyped).returns(String) }
264+
def link_source(spec)
265+
path_spec = normalise_path_spec(spec)
266+
return path_spec.fetch("path") if path_spec["base"] == "relative"
267+
268+
resolve_path(path_spec).to_s
269+
end
270+
271+
sig { params(spec: T.untyped).returns(Step) }
272+
def normalise_path_spec(spec)
273+
case spec
274+
when Hash
275+
T.cast(Utils.deep_stringify_symbols(spec), Step)
276+
else
277+
{ "path" => spec.to_s }
278+
end
279+
end
280+
281+
sig { params(base: String, formula: T.nilable(String)).returns(Pathname) }
282+
def root_path(base, formula)
283+
case base
284+
when "home"
285+
Pathname(Dir.home)
286+
when "homebrew_prefix"
287+
HOMEBREW_PREFIX
288+
when "formula_pkgetc"
289+
formula_base(formula, :pkgetc)
290+
when "formula_opt_prefix"
291+
formula_base(formula, :opt_prefix)
292+
else
293+
context_path(base)
294+
end
295+
end
296+
297+
sig { params(base: String).returns(Pathname) }
298+
def context_path(base)
299+
method = base.to_sym
300+
if @context.respond_to?(method)
301+
Pathname(T.unsafe(@context).public_send(method))
302+
elsif @context.respond_to?(:config) && T.unsafe(@context.config).respond_to?(method)
303+
Pathname(T.unsafe(@context.config).public_send(method))
304+
else
305+
raise ArgumentError, "unknown install step base: #{base}"
306+
end
307+
end
308+
309+
sig { params(formula: T.nilable(String), method: Symbol).returns(Pathname) }
310+
def formula_base(formula, method)
311+
raise ArgumentError, "missing formula for install step base" if formula.blank?
312+
313+
Pathname(T.unsafe(::Formula[formula]).public_send(method))
314+
end
315+
end
316+
end
317+
end

0 commit comments

Comments
 (0)