diff --git a/Library/Homebrew/cask/artifact.rb b/Library/Homebrew/cask/artifact.rb index 41f62450d5c1b..4bc895d068433 100644 --- a/Library/Homebrew/cask/artifact.rb +++ b/Library/Homebrew/cask/artifact.rb @@ -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" diff --git a/Library/Homebrew/cask/artifact/abstract_artifact.rb b/Library/Homebrew/cask/artifact/abstract_artifact.rb index 7b661a0a7fb1d..d4c1113e865b7 100644 --- a/Library/Homebrew/cask/artifact/abstract_artifact.rb +++ b/Library/Homebrew/cask/artifact/abstract_artifact.rb @@ -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, @@ -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, diff --git a/Library/Homebrew/cask/artifact/install_steps.rb b/Library/Homebrew/cask/artifact/install_steps.rb new file mode 100644 index 0000000000000..9c1ceac781152 --- /dev/null +++ b/Library/Homebrew/cask/artifact/install_steps.rb @@ -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 diff --git a/Library/Homebrew/cask/dsl.rb b/Library/Homebrew/cask/dsl.rb index 7789fe258a1e6..66bb0107502f9 100644 --- a/Library/Homebrew/cask/dsl.rb +++ b/Library/Homebrew/cask/dsl.rb @@ -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, @@ -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 @@ -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}'" diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index fa35ae3c93eb5..bfaf266adcf4f 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -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, @@ -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, ), ) diff --git a/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md b/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md index e54b1ee63a52b..8792d6cd869b9 100644 --- a/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md +++ b/Library/Homebrew/json_api_postinstall_preflight_postflight_plan.md @@ -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 @@ -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. @@ -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. @@ -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 }`. @@ -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 @@ -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`, diff --git a/Library/Homebrew/rubocops/cask/install_steps.rb b/Library/Homebrew/rubocops/cask/install_steps.rb index 6e906f6dd6949..a11612d18632c 100644 --- a/Library/Homebrew/rubocops/cask/install_steps.rb +++ b/Library/Homebrew/rubocops/cask/install_steps.rb @@ -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| diff --git a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi index c74e432e4872f..d9b2c468039f2 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/cask/cask.rbi @@ -171,9 +171,15 @@ class Cask::Cask sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def postflight(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def postflight_steps(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def preflight(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def preflight_steps(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def prefpane(*args, &block); end @@ -207,9 +213,15 @@ class Cask::Cask sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def uninstall_postflight(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def uninstall_postflight_steps(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } def uninstall_preflight(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.untyped) } + def uninstall_preflight_steps(*args, &block); end + sig { params(args: T.untyped, block: T.untyped).returns(T.nilable(::Cask::URL)) } def url(*args, &block); end diff --git a/Library/Homebrew/test/cask/artifact/install_steps_spec.rb b/Library/Homebrew/test/cask/artifact/install_steps_spec.rb new file mode 100644 index 0000000000000..7b2d9ac039e51 --- /dev/null +++ b/Library/Homebrew/test/cask/artifact/install_steps_spec.rb @@ -0,0 +1,78 @@ +# typed: false +# frozen_string_literal: true + +RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do + let(:cask) do + Cask::Cask.new("with-install-steps") do + version "1.2.3" + sha256 :no_check + url "file://#{TEST_FIXTURE_DIR}/cask/container.zip" + + preflight_steps do + mkdir_p "Prepared" + touch "Prepared/touched" + end + + postflight_steps do + mv "move-source", "Prepared/moved" + ln_s "Prepared/moved", "PreparedLink", source_base: :relative, uninstall: true + end + + uninstall_preflight_steps do + mkdir "UninstallPrepared" + touch "UninstallPrepared/touched" + end + + uninstall_postflight_steps do + move_children "UninstallPrepared", "UninstallMoved" + end + end + end + + it "runs structured steps through installer artifact phases" do + cask.staged_path.mkpath + cask.config_path.dirname.mkpath + (cask.staged_path/"move-source").write "moved" + + installer = Cask::Installer.new(cask, command: NeverSudoSystemCommand) + installer.install_artifacts + + expect(cask.staged_path/"Prepared").to be_a_directory + expect(cask.staged_path/"Prepared/touched").to exist + expect(cask.staged_path/"Prepared/moved").to exist + expect(cask.staged_path/"PreparedLink").to be_a_symlink + + installer.uninstall_artifacts + + expect(cask.staged_path/"PreparedLink").not_to exist + expect(cask.staged_path/"UninstallMoved/touched").to exist + end + + it "ignores a flight block when matching steps are defined" do + cask = nil + expect do + cask = Cask::Cask.new("with-install-steps-conflict") do + version "1.2.3" + sha256 :no_check + url "file://#{TEST_FIXTURE_DIR}/cask/container.zip" + + preflight do + touch "ruby-block-ran" + end + + preflight_steps do + touch "steps-ran" + end + end + end.to output(/`preflight` is ignored because `preflight_steps` is defined/).to_stderr + + cask = T.must(cask) + cask.staged_path.mkpath + cask.config_path.dirname.mkpath + + Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts + + expect(cask.staged_path/"ruby-block-ran").not_to exist + expect(cask.staged_path/"steps-ran").to exist + end +end diff --git a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb index ebd58e75f3785..1da291b6fe852 100644 --- a/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb +++ b/Library/Homebrew/test/cask/cask_loader/from_api_loader_spec.rb @@ -238,6 +238,10 @@ include_examples "loads from API", "with-zap", caskfile_only: false end + context "with install step stanzas" do + include_examples "loads from API", "with-install-steps", caskfile_only: false + end + context "with a preflight stanza" do include_examples "loads from API", "with-preflight", caskfile_only: true end diff --git a/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb b/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb index c264910c52e42..6b1f407f68426 100644 --- a/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb +++ b/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb @@ -52,4 +52,20 @@ end CASK end + + it "does not report simple legacy flight block file preparation" do + expect_no_offenses <<~CASK + cask "foo" do + version :latest + sha256 :no_check + + postflight do + (staged_path/"Prepared").mkpath + FileUtils.touch staged_path/"Prepared/touched" + FileUtils.mv staged_path/"source", staged_path/"target" + FileUtils.ln_s "target", staged_path/"Linked" + end + end + CASK + end end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/with-install-steps.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/with-install-steps.rb new file mode 100644 index 0000000000000..a163ab3f3a2f5 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/with-install-steps.rb @@ -0,0 +1,33 @@ +# typed: false +# frozen_string_literal: true + +cask "with-install-steps" do + version "1.2.3" + sha256 "67cdb184572d137c3fbd7adc93b707117f0bfb0096684a43f82aa75f924d2c63" + + url "file://#{TEST_FIXTURE_DIR}/cask/container.zip" + name "With Install Steps" + desc "Cask with structured install steps" + homepage "https://brew.sh/with-install-steps" + + app "container" + + preflight_steps do + mkdir_p "Prepared" + touch "Prepared/touched" + end + + postflight_steps do + mv "move-source", "Prepared/moved" + ln_s "Prepared/moved", "PreparedLink", source_base: :relative, uninstall: true + end + + uninstall_preflight_steps do + mkdir "UninstallPrepared" + touch "UninstallPrepared/touched" + end + + uninstall_postflight_steps do + move_children "UninstallPrepared", "UninstallMoved" + end +end diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index ef3d666958dc6..a8b02ebc64a58 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -94,12 +94,16 @@ Having a common order for stanzas makes casks easier to update and parse. Below stage_only preflight + preflight_steps postflight + postflight_steps uninstall_preflight + uninstall_preflight_steps uninstall_postflight + uninstall_postflight_steps uninstall @@ -179,9 +183,13 @@ Generated completion artifacts are different: `generate_completions_from_executa | [`deprecate!`](#stanza-deprecate--disable) | no | Date as a string in `YYYY-MM-DD` format and a string or symbol providing a reason. | | [`disable!`](#stanza-deprecate--disable) | no | Date as a string in `YYYY-MM-DD` format and a string or symbol providing a reason. | | `preflight` | yes | Ruby block containing preflight install operations (needed only in very rare cases). | +| `preflight_steps` | yes | Declarative file preparation steps run before artifact installation. | | [`postflight`](#stanza-flight) | yes | Ruby block containing postflight install operations. | +| `postflight_steps` | yes | Declarative file preparation steps run after artifact installation. | | `uninstall_preflight` | yes | Ruby block containing preflight uninstall operations (needed only in very rare cases). | +| `uninstall_preflight_steps` | yes | Declarative file preparation steps run before artifact uninstallation. | | `uninstall_postflight` | yes | Ruby block containing postflight uninstall operations. | +| `uninstall_postflight_steps` | yes | Declarative file preparation steps run after artifact uninstallation. | | [`language`](#stanza-language) | required | Ruby block, called with language code parameters, containing other stanzas and/or a return value. | | `container nested:` | no | Relative path to an inner container that must be extracted before moving on with the installation. This allows for support of `.dmg` inside `.tar`, `.zip` inside `.dmg`, etc. (Example: [blocs.rb](https://github.com/Homebrew/homebrew-cask/blob/aa461148bbb5119af26b82cccf5003e2b4e50d95/Casks/b/blocs.rb#L17-L19)) | | `container type:` | no | Symbol to override container-type autodetect. May be one of: `:air`, `:bz2`, `:cab`, `:dmg`, `:generic_unar`, `:gzip`, `:otf`, `:pkg`, `:rar`, `:seven_zip`, `:sit`, `:tar`, `:ttf`, `:xar`, `:zip`, `:naked`. (Example: [parse.rb](https://github.com/Homebrew/homebrew-cask/blob/aa461148bbb5119af26b82cccf5003e2b4e50d95/Casks/p/parse.rb#L10)) | @@ -555,6 +563,22 @@ Refer to [Deprecating, Disabling and Removing](Deprecating-Disabling-and-Removin The stanzas `preflight`, `postflight`, `uninstall_preflight`, and `uninstall_postflight` define operations to be run before or after installation or uninstallation. +For simple file preparation, prefer `preflight_steps`, `postflight_steps`, `uninstall_preflight_steps` or `uninstall_postflight_steps`. These steps are stored in the JSON API and avoid loading cask Ruby for common operations. + +```ruby +preflight_steps do + mkdir_p "Shared" + touch "Shared/state" +end + +postflight_steps do + mv "payload", "Shared/payload" + ln_s "Shared/payload", "Payload", source_base: :relative +end +``` + +`mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `symlink`, `ln_s` and `ln_sf` are available in steps blocks. Relative paths default to `staged_path` for `base:`, `source_base:` and `target_base:`. Symlink steps can use `uninstall: true` to remove the symlink during uninstall. A steps block may only contain those step calls with literal arguments; it cannot call the wider cask DSL or arbitrary Ruby code. Each phase may define either its Ruby flight block or its matching steps block, not both. + Flight blocks are not currently run in the cask sandbox. They should be written as though they may be sandboxed in the future: prefer the mini-DSL helpers below and keep filesystem writes limited to paths owned by the cask. #### Evaluation of blocks is always deferred