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
2 changes: 2 additions & 0 deletions Library/Homebrew/api/formula_struct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

require "service"
require "utils/spdx"
require "install_steps"

module Homebrew
module API
Expand Down Expand Up @@ -104,6 +105,7 @@ def self.from_hash(formula_hash)
const :no_autobump_args, T::Hash[Symbol, T.any(String, Symbol)], default: {}
const :oldnames, T::Array[String], default: []
const :post_install_defined, T::Boolean, default: false
const :post_install_steps, Homebrew::InstallSteps::Steps, default: []
const :pour_bottle_args, T::Hash[Symbol, Symbol], default: {}
const :revision, Integer, default: 0
const :ruby_source_checksum, String
Expand Down
5 changes: 4 additions & 1 deletion Library/Homebrew/cmd/postinstall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ def run
args.named.to_resolved_formulae.each do |f|
ohai "Postinstalling #{f}"
f.install_etc_var
if f.post_install_defined?
if f.post_install_steps_defined?
f.warn_on_post_install_steps_conflict if f.post_install_steps_conflict? && !args.quiet?
f.run_post_install_steps
elsif f.post_install_defined?
fi = FormulaInstaller.new(f, **{ debug: args.debug?, quiet: args.quiet?, verbose: args.verbose? }.compact)
fi.post_install
else
Expand Down
72 changes: 72 additions & 0 deletions Library/Homebrew/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
require "tab"
require "mktemp"
require "find"
require "install_steps"
require "utils/spdx"
require "on_system"
require "api"
Expand Down Expand Up @@ -636,6 +637,11 @@ def bottle_for_tag(tag = nil)
# @see .api_source
delegate api_source: :"self.class"

# The post-install steps.
# @!method post_install_steps
# @see .post_install_steps
delegate post_install_steps: :"self.class"

sig { void }
def update_head_version
return unless head?
Expand Down Expand Up @@ -1595,6 +1601,19 @@ def post_install_defined?
method(:post_install).owner != Formula
end

sig { returns(T::Boolean) }
def post_install_steps_defined? = self.class.post_install_steps_defined?

sig { returns(T::Boolean) }
def post_install_steps_conflict?
post_install_steps_defined? && post_install_defined?
end
Comment thread
MikeMcQuaid marked this conversation as resolved.

sig { void }
def warn_on_post_install_steps_conflict
opoo "#{full_name}: `post_install` is ignored because `post_install_steps` are defined!"
end

sig { void }
def install_etc_var
etc_var_dirs = [bottle_prefix/"etc", bottle_prefix/"var"]
Expand All @@ -1608,6 +1627,21 @@ def install_etc_var
end
end

sig { void }
def run_post_install_steps
return if post_install_steps.empty?

@prefix_returns_versioned_prefix = T.let(true, T.nilable(T::Boolean))

begin
with_logging("post_install_steps") do
Homebrew::InstallSteps::Runner.new(context: self).run(post_install_steps)
end
ensure
@prefix_returns_versioned_prefix = T.let(false, T.nilable(T::Boolean))
end
end

sig { void }
def run_post_install
@prefix_returns_versioned_prefix = T.let(true, T.nilable(T::Boolean))
Expand Down Expand Up @@ -2936,6 +2970,7 @@ def to_hash
"disable_replacement_formula" => disable_replacement_formula,
"disable_replacement_cask" => disable_replacement_cask,
"disable_args" => disable_args,
"post_install_steps" => post_install_steps,
"post_install_defined" => post_install_defined?,
"service" => (service.to_hash if service?),
"tap_git_head" => tap_git_head,
Expand Down Expand Up @@ -3735,6 +3770,8 @@ def inherited(child)
@conflicts = T.let([], T.nilable(T::Array[FormulaConflict]))
@skip_clean_paths = T.let(Set.new, T.nilable(T::Set[T.any(String, Symbol)]))
@link_overwrite_paths = T.let(Set.new, T.nilable(T::Set[String]))
@post_install_steps = T.let([], T.nilable(Homebrew::InstallSteps::Steps))
@post_install_steps_defined = T.let(false, T.nilable(T::Boolean))
@loaded_from_api = T.let(false, T.nilable(T::Boolean))
@loaded_from_internal_api = T.let(false, T.nilable(T::Boolean))
@api_source = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
Expand All @@ -3754,6 +3791,7 @@ def freeze
@conflicts.freeze
@skip_clean_paths.freeze
@link_overwrite_paths.freeze
@post_install_steps.freeze
@preserve_rpath&.freeze
super
end
Expand Down Expand Up @@ -3935,6 +3973,40 @@ def network_access_allowed?(phase)
env_var.nil? ? network_access_allowed[phase] : env_var == "allow"
end

sig { returns(T::Boolean) }
def post_install_steps_defined? = @post_install_steps_defined == true

# Declarative steps to run after bottle installation.
#
# ### Example
#
# ```ruby
# post_install_steps do
# mkdir "log/foo", base: :var
# end
# ```
#
# @api public
sig { params(steps: T.untyped, block: T.nilable(T.proc.void)).returns(Homebrew::InstallSteps::Steps) }
def post_install_steps(*steps, &block)
current_steps = @post_install_steps || []
return current_steps if steps.empty? && block.nil?

@post_install_steps_defined = T.let(true, T.nilable(T::Boolean))
current_steps.concat(
if block
Homebrew::InstallSteps::DSL.build(
default_base: :var,
default_source_base: :prefix,
default_target_base: :prefix,
&block
)
else
Homebrew::InstallSteps::DSL.normalise_steps(steps)
end,
)
end

# The homepage for the software. Used by users to get more information
# about the software and Homebrew maintainers as a point of contact for
# e.g. submitting patches.
Expand Down
7 changes: 6 additions & 1 deletion Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,12 @@ def finish
end
else
formula.install_etc_var
post_install if formula.post_install_defined?
if formula.post_install_steps_defined?
formula.warn_on_post_install_steps_conflict if formula.post_install_steps_conflict? && !quiet?
formula.run_post_install_steps
elsif formula.post_install_defined?
post_install
end
end

keg.prepare_debug_symbols if debug_symbols?
Expand Down
6 changes: 6 additions & 0 deletions Library/Homebrew/formulary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ def self.load_formula_from_struct!(name, formula_struct, api_source:, tap_git_he
link_overwrite path
end

@post_install_steps = T.let(
formula_struct.post_install_steps,
T.nilable(Homebrew::InstallSteps::Steps),
)
@post_install_steps_defined = T.let(formula_struct.post_install_steps.present?, T.nilable(T::Boolean))

define_method(:install) do
raise NotImplementedError, "Cannot build from source from abstract formula."
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,10 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
`_steps` blocks literal-only; when a phase gets wired in, add the runtime
warning that steps win over the legacy Ruby block; add conservative
autocorrection only where every legacy statement maps mechanically.
- [ ] PR 2, formula `post_install_steps`.
- [x] PR 2, formula `post_install_steps`.
Commit: `Add formula install steps`.
Scope: formula DSL, formula JSON API data, API formula loading, installer and
`brew postinstall` execution, formula cookbook docs, formula fixture and
formula-specific autocorrection.
`brew postinstall` execution, formula cookbook docs and formula fixture.
Estimated existing formulae affected: `178` formulae currently define
`post_install`. The first useful conversion surface is roughly `73` formulae
creating shared directories and `9` touching marker or lock files; parts of
Expand All @@ -112,7 +111,8 @@ Local scan source: `homebrew/cask` at `4ed4e04eaa5`.
Notes for implementation: default `mkdir`/`touch` to `var` and source/target
paths to `prefix`; expose the ordered array through `FormulaStruct`; make
`post_install_steps` take precedence over `post_install`; document that the
two forms must not be mixed.
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.
Commit: `Add cask install steps`.
Scope: cask artifacts for `preflight_steps`, `postflight_steps`,
Expand Down
3 changes: 2 additions & 1 deletion Library/Homebrew/rubocops/install_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def audit_formula(formula_nodes)
return if (body_node = formula_nodes.body_node).nil?

post_install_steps_block = find_block(body_node, :post_install_steps)
if post_install_steps_block && find_method_def(body_node, :post_install)
post_install_method = find_method_def(body_node, :post_install)
if post_install_steps_block && post_install_method
offending_node(post_install_steps_block)
problem CONFLICT_MSG
end
Expand Down
1 change: 1 addition & 0 deletions Library/Homebrew/rubocops/shared/api_annotation_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module ApiAnnotationHelper
"desc" => "formula.rb",
"env_script_all_files" => "extend/pathname.rb",
"fails_with" => "formula.rb",
"post_install_steps" => "formula.rb",
"head" => "formula.rb",
"homepage" => "formula.rb",
"install_symlink" => "extend/pathname.rb",
Expand Down
3 changes: 3 additions & 0 deletions Library/Homebrew/sorbet/rbi/dsl/formula.rbi

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

18 changes: 18 additions & 0 deletions Library/Homebrew/test/api/formula_struct_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -287,5 +287,23 @@ def build_formula_struct(checksums)

expect(restored).to eq(original)
end

it "serializes post-install steps", :needs_macos do
original = klass.new(
desc: "install steps test",
homepage: "https://example.com",
license: "MIT",
ruby_source_checksum: "abc123",
stable_version: "1.0.0",
post_install_steps: [
{ "type" => "mkdir_p", "path" => { "base" => "var", "path" => "log/foo" } },
],
)

serialized = original.serialize(bottle_tag: Utils::Bottles::Tag.from_symbol(:arm64_sequoia))
restored = klass.deserialize(serialized, bottle_tag: Utils::Bottles::Tag.from_symbol(:arm64_sequoia))

expect(restored.post_install_steps).to eq(original.post_install_steps)
end
end
end
91 changes: 91 additions & 0 deletions Library/Homebrew/test/formula_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,97 @@ def post_install
expect(f2).not_to have_post_install_defined
end

specify "#post_install_steps" do
f = formula do
url "foo-1.0"

post_install_steps do
mkdir_p "log/foo"
touch "foo/marker"
mv "move-source", "move-target"
move_children "children-source", "children-target"
ln_s "move-target", "linked-target", source_base: :relative, uninstall: true
end
end

expect(f.post_install_steps).to eq([
{ "type" => "mkdir_p", "path" => { "base" => "var", "path" => "log/foo" } },
{ "type" => "touch", "path" => { "base" => "var", "path" => "foo/marker" } },
{
"type" => "move",
"source" => { "base" => "prefix", "path" => "move-source" },
"target" => { "base" => "prefix", "path" => "move-target" },
},
{
"type" => "move_children",
"source" => { "base" => "prefix", "path" => "children-source" },
"target" => { "base" => "prefix", "path" => "children-target" },
},
{
"type" => "symlink",
"source" => { "base" => "relative", "path" => "move-target" },
"target" => { "base" => "prefix", "path" => "linked-target" },
"uninstall" => true,
},
])
expect(f.post_install_steps_defined?).to be(true)
expect(f.to_hash["post_install_steps"]).to eq(f.post_install_steps)
end

specify "#post_install_steps_defined? with an empty block" do
f = formula do
url "foo-1.0"

# This intentionally declares no steps to test definition tracking.
# rubocop:disable Lint/EmptyBlock
post_install_steps do
end
# rubocop:enable Lint/EmptyBlock
end

expect(f.post_install_steps).to be_empty
expect(f.post_install_steps_defined?).to be(true)
end

specify "#post_install_steps_conflict?" do
f = formula do
url "foo-1.0"

# This intentionally declares no steps to test conflict tracking.
# rubocop:disable Lint/EmptyBlock
post_install_steps do
end
# rubocop:enable Lint/EmptyBlock

def post_install; end
end

expect(f.post_install_steps_conflict?).to be(true)
end

specify "#run_post_install_steps uses the versioned prefix" do
f = formula "post-install-steps-prefix" do
url "foo-1.0"

post_install_steps do
ln_s "source", "linked", source_base: :prefix, target_base: :prefix
end
end

versioned_prefix = f.rack/f.pkg_version.to_s
FileUtils.rm_f f.opt_prefix
versioned_prefix.mkpath
f.opt_prefix.parent.mkpath
FileUtils.ln_s versioned_prefix, f.opt_prefix

f.run_post_install_steps

expect((versioned_prefix/"linked").readlink).to eq(versioned_prefix/"source")
ensure
FileUtils.rm_f f.opt_prefix
FileUtils.rm_rf f.rack
end

describe "#install_etc_var" do
let(:f) do
formula "config-upgrade" do
Expand Down
15 changes: 15 additions & 0 deletions Library/Homebrew/test/rubocops/install_steps_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,19 @@ class Foo < Formula
end
RUBY
end

it "does not report simple legacy `post_install` file preparation" do
expect_no_offenses(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"

def post_install
(var/"log/foo").mkpath
FileUtils.touch var/"foo/state"
FileUtils.mv prefix/"move-source", prefix/"move-target"
FileUtils.ln_sf "move-target", prefix/"linked-target"
end
end
RUBY
end
end
21 changes: 21 additions & 0 deletions Library/Homebrew/test/support/fixtures/formulae/install-steps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# typed: false
# frozen_string_literal: true

class InstallSteps < Formula
# Sorbet type members are mutable by design and cannot be frozen.
# rubocop:disable Style/MutableConstant
Cache = type_template { { fixed: T::Hash[Symbol, T.untyped] } }
# rubocop:enable Style/MutableConstant

desc "Formula with structured install steps"
homepage "https://brew.sh/install-steps"
url "https://brew.sh/install-steps-1.0"

post_install_steps do
mkdir_p "log/install-steps"
touch "install-steps/state"
mv "move-source", "move-target"
move_children "move-children-source", "move-children-target"
ln_sf "move-target", "linked-target", source_base: :relative, uninstall: true
end
end
Loading
Loading