Skip to content

Commit 875a2c4

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 875a2c4

11 files changed

Lines changed: 1093 additions & 4 deletions

Library/Homebrew/install_steps.rb

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

0 commit comments

Comments
 (0)