Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Library/Homebrew/cask/artifact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "cask/artifact/binary"
require "cask/artifact/colorpicker"
require "cask/artifact/dictionary"
require "cask/artifact/install_steps"
require "cask/artifact/font"
require "cask/artifact/input_method"
require "cask/artifact/installer"
Expand Down
4 changes: 4 additions & 0 deletions Library/Homebrew/cask/artifact/abstract_artifact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def sort_order
@sort_order ||= T.let(
[
PreflightBlock,
PreflightSteps,
UninstallPreflightSteps,
# The `uninstall` stanza should be run first, as it may
# depend on other artifacts still being installed.
Uninstall,
Expand Down Expand Up @@ -101,6 +103,8 @@ def sort_order
],
Binary,
Manpage,
PostflightSteps,
UninstallPostflightSteps,
PostflightBlock,
Zap,
].each_with_index.flat_map { |classes, i| Array(classes).map { |c| [c, i] } }.to_h,
Expand Down
75 changes: 75 additions & 0 deletions Library/Homebrew/cask/artifact/install_steps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# typed: strict
# frozen_string_literal: true

require "cask/artifact/abstract_artifact"
require "install_steps"

module Cask
module Artifact
class AbstractInstallSteps < AbstractArtifact
abstract!

sig { params(cask: Cask, steps: Homebrew::InstallSteps::Steps).void }
def initialize(cask, steps)
super
@steps = T.let(Homebrew::InstallSteps::DSL.normalise_steps(steps), Homebrew::InstallSteps::Steps)
end

sig { returns(Homebrew::InstallSteps::Steps) }
attr_reader :steps

sig { override.returns(T::Array[T.anything]) }
def to_args = [{ steps: }]

sig { override.returns(String) }
def summarize
::Utils.pluralize("install step", steps.length, include_count: true)
end

private

sig { returns(Homebrew::InstallSteps::Runner) }
def runner
Homebrew::InstallSteps::Runner.new(context: cask)
end
end

class PreflightSteps < AbstractInstallSteps
sig { params(_options: T.anything).void }
def install_phase(**_options)
runner.run(steps)
end

sig { params(_options: T.anything).void }
def uninstall_phase(**_options)
runner.run(steps, phase: :uninstall)
end
end

class PostflightSteps < AbstractInstallSteps
sig { params(_options: T.anything).void }
def install_phase(**_options)
runner.run(steps)
end

sig { params(_options: T.anything).void }
def uninstall_phase(**_options)
runner.run(steps, phase: :uninstall)
end
end

class UninstallPreflightSteps < AbstractInstallSteps
sig { params(_options: T.anything).void }
def uninstall_phase(**_options)
runner.run(steps)
end
end

class UninstallPostflightSteps < AbstractInstallSteps
sig { params(_options: T.anything).void }
def uninstall_phase(**_options)
runner.run(steps)
end
end
end
end
76 changes: 75 additions & 1 deletion Library/Homebrew/cask/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ class DSL
Artifact::PostflightBlock,
].freeze

INSTALL_STEP_ARTIFACT_CLASSES = [
Artifact::PreflightSteps,
Artifact::PostflightSteps,
Artifact::UninstallPreflightSteps,
Artifact::UninstallPostflightSteps,
].freeze

InstallStepFlightBlockClasses = T.type_alias do
T::Hash[
T.class_of(Artifact::AbstractInstallSteps),
[T.class_of(Artifact::AbstractFlightBlock), Symbol],
]
end

INSTALL_STEP_FLIGHT_BLOCK_CLASSES = T.let({
Artifact::PreflightSteps => [Artifact::PreflightBlock, :preflight],
Artifact::PostflightSteps => [Artifact::PostflightBlock, :postflight],
Artifact::UninstallPreflightSteps => [Artifact::PreflightBlock, :uninstall_preflight],
Artifact::UninstallPostflightSteps => [Artifact::PostflightBlock, :uninstall_postflight],
}.freeze, InstallStepFlightBlockClasses)

DSL_METHODS = T.let(Set.new([
:arch,
:artifacts,
Expand Down Expand Up @@ -121,6 +142,7 @@ class DSL
*ORDINARY_ARTIFACT_CLASSES.map(&:dsl_key),
*ACTIVATABLE_ARTIFACT_CLASSES.map(&:dsl_key),
*ARTIFACT_BLOCK_CLASSES.flat_map { |klass| [klass.dsl_key, klass.uninstall_dsl_key] },
*INSTALL_STEP_ARTIFACT_CLASSES.map(&:dsl_key),
]).freeze, T::Set[Symbol])

include OnSystem::MacOSAndLinux
Expand Down Expand Up @@ -798,11 +820,63 @@ def disable!(date:, because:, replacement: nil, replacement_formula: nil, replac
[klass.dsl_key, klass.uninstall_dsl_key].each do |dsl_key|
define_method(dsl_key) do |&block|
T.bind(self, DSL)
artifacts.add(klass.new(cask, dsl_key => block))
if install_step_artifact_defined?(dsl_key)
warn_on_install_step_conflict(dsl_key, T.must(install_step_artifact_class(dsl_key)).dsl_key)
else
artifacts.add(klass.new(cask, dsl_key => block))
end
end
end
end

INSTALL_STEP_ARTIFACT_CLASSES.each do |klass|
define_method(klass.dsl_key) do |steps = nil, **kwargs, &block|
T.bind(self, DSL)
steps = if block
Homebrew::InstallSteps::DSL.build(default_base: :staged_path, default_source_base: :staged_path,
default_target_base: :staged_path, &block)
else
Homebrew::InstallSteps::DSL.normalise_steps([kwargs[:steps] || steps].flatten.compact)
end
remove_conflicting_flight_blocks(klass)
artifacts.add(klass.new(cask, steps))
end
end

sig { params(dsl_key: Symbol).returns(T::Boolean) }
def install_step_artifact_defined?(dsl_key)
return false unless (klass = install_step_artifact_class(dsl_key))

artifacts.any?(klass)
end

sig { params(dsl_key: Symbol).returns(T.nilable(T.class_of(Artifact::AbstractInstallSteps))) }
def install_step_artifact_class(dsl_key)
INSTALL_STEP_FLIGHT_BLOCK_CLASSES.find do |_step_class, (_block_class, block_dsl_key)|
block_dsl_key == dsl_key
end&.first
end

sig { params(klass: T.class_of(Artifact::AbstractInstallSteps)).void }
def remove_conflicting_flight_blocks(klass)
flight_block_class, dsl_key = INSTALL_STEP_FLIGHT_BLOCK_CLASSES.fetch(klass)
conflicting_flight_blocks = artifacts.select do |artifact|
next false unless artifact.is_a?(flight_block_class)

artifact.directives.key?(dsl_key)
end

conflicting_flight_blocks.each do |artifact|
warn_on_install_step_conflict(dsl_key, klass.dsl_key)
artifacts.delete(artifact)
end
end

sig { params(dsl_key: Symbol, steps_key: Symbol).void }
def warn_on_install_step_conflict(dsl_key, steps_key)
opoo "#{token}: `#{dsl_key}` is ignored because `#{steps_key}` is defined!"
end

sig { override.params(method: Symbol, _args: T.anything).returns(T.noreturn) }
def method_missing(method, *_args)
raise NoMethodError, "undefined method '#{method}' for Cask '#{token}'"
Expand Down
6 changes: 6 additions & 0 deletions Library/Homebrew/cask/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ def install_artifacts(predecessor: nil)
Artifact::KeyboardLayout,
Artifact::Mdimporter,
Artifact::Moved,
Artifact::PostflightSteps,
Artifact::PreflightSteps,
Artifact::Pkg,
Artifact::Qlplugin,
Artifact::Symlinked,
Expand Down Expand Up @@ -654,9 +656,13 @@ def uninstall_artifacts(clear: false, successor: nil, quit: true)
Artifact::GeneratedCompletion,
Artifact::KeyboardLayout,
Artifact::Moved,
Artifact::PostflightSteps,
Artifact::PreflightSteps,
Artifact::Qlplugin,
Artifact::Symlinked,
Artifact::Uninstall,
Artifact::UninstallPostflightSteps,
Artifact::UninstallPreflightSteps,
),
)

Expand Down
43 changes: 38 additions & 5 deletions Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ steps run if present; the legacy block is ignored with a warning. Post-install
or postflight steps are not sandboxed for this iteration because they run only
Homebrew-owned structured operations. The runner shape leaves room to sandbox
future step types that invoke non-Homebrew code.
Future cask work should sandbox all `*flight` run scripts from non-Homebrew and
non-system sources, for example scripts shipped by upstream artifacts.

RuboCop autocorrection converts the simplest existing `post_install` and
`*flight` Ruby blocks to steps blocks when every statement is a supported file
Expand All @@ -38,6 +40,12 @@ from the matching legacy Ruby pattern where possible.
Local scan source: `homebrew/core` at `fb0ca6682b4`.

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

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

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

## API Source Download Gates

Formula JSON API installs need to preserve `post_install` because it is the
only install-time Ruby hook recorded for bottle installs. The hook runs from
the formula stored in the installed keg, while source builds and local patch
handling use `Homebrew::API::Formula.source_download_formula` for build-time
reasons outside this post-install DSL work.

Cask JSON API installs use `Homebrew::API::Cask.source_download_cask` when
`Cask#caskfile_only?` is true. Today that is true when a cask has any legacy
`preflight`, `postflight`, `uninstall_preflight` or `uninstall_postflight`
block, or when it has language blocks. Legacy flight blocks need the source
because API data only records that a block exists, not the Ruby body. Language
blocks need the source because the API stores available language codes, but not
the selected block return value or stanza effects; language-specific URLs must
be resolved before the download can be enqueued.

## Install Step Examples

- `languagetool`: `post_install_steps { mkdir "log/languagetool", base: :var }`.
Expand Down Expand Up @@ -113,12 +144,12 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
`post_install_steps` take precedence over `post_install`; document that the
two forms must not be mixed. Keep the tap-wide autocorrect audit in a
follow-up commit so the implementation can land before converted formulae.
- [ ] PR 3, cask flight steps.
- [x] PR 3, cask flight steps.
Commit: `Add cask install steps`.
Scope: cask artifacts for `preflight_steps`, `postflight_steps`,
`uninstall_preflight_steps` and `uninstall_postflight_steps`, cask API
serialisation through artifact data, installer casts, cask cookbook docs,
cask fixture/API loader coverage and cask-specific autocorrection.
cask fixture/API loader coverage.
Estimated existing casks affected: `181` casks currently use flight blocks.
The first useful conversion surface is roughly `78` casks that create/touch
files or directories and the supported subset of `27` casks that move or
Expand All @@ -127,7 +158,9 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
Notes for implementation: default all relative cask paths to `staged_path`;
keep steps as normal cask artifacts so API loader round-trips work; make
steps remove/override the matching Ruby flight artifact with a warning; keep
`uninstall: true` symlink cleanup available for install-phase steps.
`uninstall: true` symlink cleanup available for install-phase steps. Keep
the tap-wide autocorrect audit in a follow-up commit so the implementation
can land before converted casks.
- [ ] PR 4, desktop and cache rebuild actions.
Estimated existing formulae/casks affected: about `35` formulae run rebuild
tools such as `glib-compile-schemas`, `gtk*-update-icon-cache`,
Expand Down
9 changes: 6 additions & 3 deletions Library/Homebrew/rubocops/cask/install_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ def on_cask(cask_block)
stanzas = cask_block.stanzas
INSTALL_STEP_PAIRS.each do |flight_block, steps_block|
next unless (flight_stanza = stanzas.find { |stanza| stanza.stanza_name == flight_block })
next unless (steps_stanza = stanzas.find { |stanza| stanza.stanza_name == steps_block })

add_offense(steps_stanza.source_range,
message: "`#{flight_stanza.stanza_name}` and `#{steps_block}` cannot both be used.")
steps_stanza = stanzas.find { |stanza| stanza.stanza_name == steps_block }

if steps_stanza
add_offense(steps_stanza.source_range,
message: "`#{flight_stanza.stanza_name}` and `#{steps_block}` cannot both be used.")
end
end

stanzas.each do |stanza|
Expand Down
12 changes: 12 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading