Skip to content

Commit f94f9c7

Browse files
committed
Add cask install steps
- Expose structured steps for cask flight phases and API data. - Prefer steps over matching Ruby flight blocks with warnings. - Document cask usage and keep conversion audits separate.
1 parent e05e1d5 commit f94f9c7

13 files changed

Lines changed: 370 additions & 9 deletions

File tree

Library/Homebrew/cask/artifact.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
require "cask/artifact/binary"
99
require "cask/artifact/colorpicker"
1010
require "cask/artifact/dictionary"
11+
require "cask/artifact/install_steps"
1112
require "cask/artifact/font"
1213
require "cask/artifact/input_method"
1314
require "cask/artifact/installer"

Library/Homebrew/cask/artifact/abstract_artifact.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ def sort_order
6969
@sort_order ||= T.let(
7070
[
7171
PreflightBlock,
72+
PreflightSteps,
73+
UninstallPreflightSteps,
7274
# The `uninstall` stanza should be run first, as it may
7375
# depend on other artifacts still being installed.
7476
Uninstall,
@@ -99,6 +101,8 @@ def sort_order
99101
],
100102
Binary,
101103
Manpage,
104+
PostflightSteps,
105+
UninstallPostflightSteps,
102106
PostflightBlock,
103107
Zap,
104108
].each_with_index.flat_map { |classes, i| Array(classes).map { |c| [c, i] } }.to_h,
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "cask/artifact/abstract_artifact"
5+
require "install_steps"
6+
7+
module Cask
8+
module Artifact
9+
class AbstractInstallSteps < AbstractArtifact
10+
abstract!
11+
12+
sig { params(cask: Cask, steps: Homebrew::InstallSteps::Steps).void }
13+
def initialize(cask, steps)
14+
super
15+
@steps = T.let(Homebrew::InstallSteps::DSL.normalise_steps(steps), Homebrew::InstallSteps::Steps)
16+
end
17+
18+
sig { returns(Homebrew::InstallSteps::Steps) }
19+
attr_reader :steps
20+
21+
sig { override.returns(T::Array[T.anything]) }
22+
def to_args = [{ steps: }]
23+
24+
sig { override.returns(String) }
25+
def summarize
26+
::Utils.pluralize("install step", steps.length, include_count: true)
27+
end
28+
29+
private
30+
31+
sig { returns(Homebrew::InstallSteps::Runner) }
32+
def runner
33+
Homebrew::InstallSteps::Runner.new(context: cask)
34+
end
35+
end
36+
37+
class PreflightSteps < AbstractInstallSteps
38+
sig { params(_options: T.anything).void }
39+
def install_phase(**_options)
40+
runner.run(steps)
41+
end
42+
43+
sig { params(_options: T.anything).void }
44+
def uninstall_phase(**_options)
45+
runner.run(steps, phase: :uninstall)
46+
end
47+
end
48+
49+
class PostflightSteps < AbstractInstallSteps
50+
sig { params(_options: T.anything).void }
51+
def install_phase(**_options)
52+
runner.run(steps)
53+
end
54+
55+
sig { params(_options: T.anything).void }
56+
def uninstall_phase(**_options)
57+
runner.run(steps, phase: :uninstall)
58+
end
59+
end
60+
61+
class UninstallPreflightSteps < AbstractInstallSteps
62+
sig { params(_options: T.anything).void }
63+
def uninstall_phase(**_options)
64+
runner.run(steps)
65+
end
66+
end
67+
68+
class UninstallPostflightSteps < AbstractInstallSteps
69+
sig { params(_options: T.anything).void }
70+
def uninstall_phase(**_options)
71+
runner.run(steps)
72+
end
73+
end
74+
end
75+
end

Library/Homebrew/cask/dsl.rb

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,27 @@ class DSL
7676
Artifact::PostflightBlock,
7777
].freeze
7878

79+
INSTALL_STEP_ARTIFACT_CLASSES = [
80+
Artifact::PreflightSteps,
81+
Artifact::PostflightSteps,
82+
Artifact::UninstallPreflightSteps,
83+
Artifact::UninstallPostflightSteps,
84+
].freeze
85+
86+
InstallStepFlightBlockClasses = T.type_alias do
87+
T::Hash[
88+
T.class_of(Artifact::AbstractInstallSteps),
89+
[T.class_of(Artifact::AbstractFlightBlock), Symbol],
90+
]
91+
end
92+
93+
INSTALL_STEP_FLIGHT_BLOCK_CLASSES = T.let({
94+
Artifact::PreflightSteps => [Artifact::PreflightBlock, :preflight],
95+
Artifact::PostflightSteps => [Artifact::PostflightBlock, :postflight],
96+
Artifact::UninstallPreflightSteps => [Artifact::PreflightBlock, :uninstall_preflight],
97+
Artifact::UninstallPostflightSteps => [Artifact::PostflightBlock, :uninstall_postflight],
98+
}.freeze, InstallStepFlightBlockClasses)
99+
79100
DSL_METHODS = T.let(Set.new([
80101
:arch,
81102
:artifacts,
@@ -121,6 +142,7 @@ class DSL
121142
*ORDINARY_ARTIFACT_CLASSES.map(&:dsl_key),
122143
*ACTIVATABLE_ARTIFACT_CLASSES.map(&:dsl_key),
123144
*ARTIFACT_BLOCK_CLASSES.flat_map { |klass| [klass.dsl_key, klass.uninstall_dsl_key] },
145+
*INSTALL_STEP_ARTIFACT_CLASSES.map(&:dsl_key),
124146
]).freeze, T::Set[Symbol])
125147

126148
include OnSystem::MacOSAndLinux
@@ -798,11 +820,63 @@ def disable!(date:, because:, replacement: nil, replacement_formula: nil, replac
798820
[klass.dsl_key, klass.uninstall_dsl_key].each do |dsl_key|
799821
define_method(dsl_key) do |&block|
800822
T.bind(self, DSL)
801-
artifacts.add(klass.new(cask, dsl_key => block))
823+
if install_step_artifact_defined?(dsl_key)
824+
warn_on_install_step_conflict(dsl_key, T.must(install_step_artifact_class(dsl_key)).dsl_key)
825+
else
826+
artifacts.add(klass.new(cask, dsl_key => block))
827+
end
828+
end
829+
end
830+
end
831+
832+
INSTALL_STEP_ARTIFACT_CLASSES.each do |klass|
833+
define_method(klass.dsl_key) do |steps = nil, **kwargs, &block|
834+
T.bind(self, DSL)
835+
steps = if block
836+
Homebrew::InstallSteps::DSL.build(default_base: :staged_path, default_source_base: :staged_path,
837+
default_target_base: :staged_path, &block)
838+
else
839+
Homebrew::InstallSteps::DSL.normalise_steps([kwargs[:steps] || steps].flatten.compact)
802840
end
841+
remove_conflicting_flight_blocks(klass)
842+
artifacts.add(klass.new(cask, steps))
843+
end
844+
end
845+
846+
sig { params(dsl_key: Symbol).returns(T::Boolean) }
847+
def install_step_artifact_defined?(dsl_key)
848+
return false unless (klass = install_step_artifact_class(dsl_key))
849+
850+
artifacts.any?(klass)
851+
end
852+
853+
sig { params(dsl_key: Symbol).returns(T.nilable(T.class_of(Artifact::AbstractInstallSteps))) }
854+
def install_step_artifact_class(dsl_key)
855+
INSTALL_STEP_FLIGHT_BLOCK_CLASSES.find do |_step_class, (_block_class, block_dsl_key)|
856+
block_dsl_key == dsl_key
857+
end&.first
858+
end
859+
860+
sig { params(klass: T.class_of(Artifact::AbstractInstallSteps)).void }
861+
def remove_conflicting_flight_blocks(klass)
862+
flight_block_class, dsl_key = INSTALL_STEP_FLIGHT_BLOCK_CLASSES.fetch(klass)
863+
conflicting_flight_blocks = artifacts.select do |artifact|
864+
next false unless artifact.is_a?(flight_block_class)
865+
866+
artifact.directives.key?(dsl_key)
867+
end
868+
869+
conflicting_flight_blocks.each do |artifact|
870+
warn_on_install_step_conflict(dsl_key, klass.dsl_key)
871+
artifacts.delete(artifact)
803872
end
804873
end
805874

875+
sig { params(dsl_key: Symbol, steps_key: Symbol).void }
876+
def warn_on_install_step_conflict(dsl_key, steps_key)
877+
opoo "#{token}: `#{dsl_key}` is ignored because `#{steps_key}` is defined!"
878+
end
879+
806880
sig { override.params(method: Symbol, _args: T.anything).returns(T.noreturn) }
807881
def method_missing(method, *_args)
808882
raise NoMethodError, "undefined method '#{method}' for Cask '#{token}'"

Library/Homebrew/cask/installer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,8 @@ def install_artifacts(predecessor: nil)
331331
Artifact::KeyboardLayout,
332332
Artifact::Mdimporter,
333333
Artifact::Moved,
334+
Artifact::PostflightSteps,
335+
Artifact::PreflightSteps,
334336
Artifact::Pkg,
335337
Artifact::Qlplugin,
336338
Artifact::Symlinked,
@@ -652,9 +654,13 @@ def uninstall_artifacts(clear: false, successor: nil, quit: true)
652654
Artifact::GeneratedCompletion,
653655
Artifact::KeyboardLayout,
654656
Artifact::Moved,
657+
Artifact::PostflightSteps,
658+
Artifact::PreflightSteps,
655659
Artifact::Qlplugin,
656660
Artifact::Symlinked,
657661
Artifact::Uninstall,
662+
Artifact::UninstallPostflightSteps,
663+
Artifact::UninstallPreflightSteps,
658664
),
659665
)
660666

Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ from the matching legacy Ruby pattern where possible.
3838
Local scan source: `homebrew/core` at `fb0ca6682b4`.
3939

4040
- `178` of `8,359` formulae define `post_install`.
41+
- `post_install_defined` is the only install-time Ruby execution flag exposed
42+
through the formula JSON API for bottle installs. I did not find a
43+
caskfile-only-style source download gate for other formula DSL at bottle
44+
install time; formula source downloads for API-loaded formulae are used for
45+
source builds, local patches and resources rather than post-install metadata
46+
gaps.
4147
- `73` create shared directories in `var`, `etc` or `HOMEBREW_PREFIX`.
4248
Examples: `glib`, `languagetool`, `mecab`.
4349
- `71` write or patch default configuration/data files.
@@ -55,8 +61,14 @@ Local scan source: `homebrew/core` at `fb0ca6682b4`.
5561

5662
Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
5763

58-
- `204` of `7,646` casks currently require the Ruby source at install time:
59-
`181` because of flight blocks and `23` because of language blocks only.
64+
- `204` of `7,646` casks currently require the Ruby source at install time
65+
through `Cask#caskfile_only?`: `181` because of legacy `*flight` blocks and
66+
`23` because of language blocks only. `27` casks have language blocks in
67+
total, so `4` have both language blocks and legacy `*flight` blocks.
68+
- I did not find other current cask install-time Ruby source download gates.
69+
Ordinary artifacts, uninstall/zap directives, caveats, dependencies and
70+
`on_*` variations are serialised through API data. `*_steps` artifacts are
71+
also serialised and should not make `caskfile_only?` true.
6072
- `78` flight blocks create directories, touch files or write small files.
6173
Examples: `86box`, `autogram`, `dante-via`.
6274
- `27` move, copy or symlink files during install or uninstall.
@@ -68,6 +80,23 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
6880
- `27` casks use language blocks. Large examples include `firefox`,
6981
`libreoffice-language-pack` and `thunderbird`.
7082

83+
## API Source Download Gates
84+
85+
Formula JSON API installs need to preserve `post_install` because it is the
86+
only install-time Ruby hook recorded for bottle installs. The hook runs from
87+
the formula stored in the installed keg, while source builds and local patch
88+
handling use `Homebrew::API::Formula.source_download_formula` for build-time
89+
reasons outside this post-install DSL work.
90+
91+
Cask JSON API installs use `Homebrew::API::Cask.source_download_cask` when
92+
`Cask#caskfile_only?` is true. Today that is true when a cask has any legacy
93+
`preflight`, `postflight`, `uninstall_preflight` or `uninstall_postflight`
94+
block, or when it has language blocks. Legacy flight blocks need the source
95+
because API data only records that a block exists, not the Ruby body. Language
96+
blocks need the source because the API stores available language codes, but not
97+
the selected block return value or stanza effects; language-specific URLs must
98+
be resolved before the download can be enqueued.
99+
71100
## Install Step Examples
72101

73102
- `languagetool`: `post_install_steps { mkdir "log/languagetool", base: :var }`.
@@ -113,12 +142,12 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
113142
paths to `prefix`; expose the ordered array through `FormulaStruct`; make
114143
`post_install_steps` take precedence over `post_install`; document that the
115144
two forms must not be mixed.
116-
- [ ] PR 3, cask flight steps.
145+
- [x] PR 3, cask flight steps.
117146
Commit: `Add cask install steps`.
118147
Scope: cask artifacts for `preflight_steps`, `postflight_steps`,
119148
`uninstall_preflight_steps` and `uninstall_postflight_steps`, cask API
120149
serialisation through artifact data, installer casts, cask cookbook docs,
121-
cask fixture/API loader coverage and cask-specific autocorrection.
150+
cask fixture/API loader coverage.
122151
Estimated existing casks affected: `181` casks currently use flight blocks.
123152
The first useful conversion surface is roughly `78` casks that create/touch
124153
files or directories and the supported subset of `27` casks that move or
@@ -127,7 +156,9 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
127156
Notes for implementation: default all relative cask paths to `staged_path`;
128157
keep steps as normal cask artifacts so API loader round-trips work; make
129158
steps remove/override the matching Ruby flight artifact with a warning; keep
130-
`uninstall: true` symlink cleanup available for install-phase steps.
159+
`uninstall: true` symlink cleanup available for install-phase steps. Keep
160+
the tap-wide autocorrect audit in a follow-up commit so the implementation
161+
can land before converted casks.
131162
- [ ] PR 4, desktop and cache rebuild actions.
132163
Estimated existing formulae/casks affected: about `35` formulae run rebuild
133164
tools such as `glib-compile-schemas`, `gtk*-update-icon-cache`,

Library/Homebrew/rubocops/cask/install_steps.rb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ def on_cask(cask_block)
2626
stanzas = cask_block.stanzas
2727
INSTALL_STEP_PAIRS.each do |flight_block, steps_block|
2828
next unless (flight_stanza = stanzas.find { |stanza| stanza.stanza_name == flight_block })
29-
next unless (steps_stanza = stanzas.find { |stanza| stanza.stanza_name == steps_block })
3029

31-
add_offense(steps_stanza.source_range,
32-
message: "`#{flight_stanza.stanza_name}` and `#{steps_block}` cannot both be used.")
30+
steps_stanza = stanzas.find { |stanza| stanza.stanza_name == steps_block }
31+
32+
if steps_stanza
33+
add_offense(steps_stanza.source_range,
34+
message: "`#{flight_stanza.stanza_name}` and `#{steps_block}` cannot both be used.")
35+
end
3336
end
3437

3538
stanzas.each do |stanza|

Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)