Add PHP configuration action

Five PHP formulae share PEAR, PECL and versioned extension configuration
that must be rebuilt after installation.

- preserve PEAR directory traversal and repair stale PECL links
- create the common PECL extension layout from `php-config`
- rewrite versioned opcache paths without changing commented settings
- cover the shared filesystem and command behaviour end to end
This commit is contained in:
Mike McQuaid
2026-07-31 09:22:13 +01:00
parent 5616730508
commit 0e956148e5
6 changed files with 159 additions and 4 deletions
+7
View File
@@ -658,6 +658,11 @@ module Homebrew
add_step("configure_clang_system")
end
sig { void }
def configure_php
add_step("configure_php")
end
private
sig { params(guard: PathSpec, block: ::T.proc.bind(DSL).void).void }
@@ -914,6 +919,8 @@ module Homebrew
run_configure_glibc_runtime
when "configure_clang_system"
run_configure_clang_system
when "configure_php"
run_configure_php
when "set_permissions"
run_set_permissions(step)
when "set_ownership"
@@ -136,6 +136,65 @@ module Homebrew
require "utils/clang"
Utils::Clang.write_system_config_files(config_dir:, macos_version:, kernel_version:, arch:)
end
sig { void }
def run_configure_php
pear_prefix = context_path("pkgshare")/"pear"
channels = [pear_prefix/".channels", pear_prefix/".channels/.alias"]
channels.select(&:directory?).each { |directory| FileUtils.chmod 0755, directory }
pear_files = %w[.depdblock .filemap .depdb .lock].map { |file| pear_prefix/file }.select(&:file?)
pear_files.concat(channels.flat_map do |directory|
directory.directory? ? directory.children.select(&:file?) : []
end)
FileUtils.chmod 0644, pear_files
pecl_path = HOMEBREW_PREFIX/"lib/php/pecl"
pecl_path.mkpath
prefix_pecl = context_path("prefix")/"pecl"
prefix_pecl.unlink if prefix_pecl.symlink?
File.symlink pecl_path, prefix_pecl unless prefix_pecl.exist?
php_basename = File.basename(run_command_output(context_path("bin")/"php-config", "--extension-dir").strip)
(pecl_path/php_basename).mkpath
version_major_minor = context_version_major_minor
raise ArgumentError, "PHP configuration requires a version" if version_major_minor.nil?
pear_dir = (context_name == "php") ? "pear" : "pear@#{version_major_minor}"
pear_path = HOMEBREW_PREFIX/"share"/pear_dir
FileUtils.cp_r "#{pear_prefix}/.", pear_path
php_ext_dir = context_path("opt_prefix")/"lib/php"/php_basename
{
"php_ini" => context_path("etc")/"php/#{version_major_minor}/php.ini",
"php_dir" => pear_path,
"doc_dir" => pear_path/"doc",
"ext_dir" => pecl_path/php_basename,
"bin_dir" => context_path("opt_prefix")/"bin",
"data_dir" => pear_path/"data",
"cfg_dir" => pear_path/"cfg",
"www_dir" => pear_path/"htdocs",
"man_dir" => HOMEBREW_PREFIX/"share/man",
"test_dir" => pear_path/"test",
"php_bin" => context_path("opt_prefix")/"bin/php",
}.each do |key, value|
value.mkpath if /(?<!bin|man)_dir$/.match?(key)
run_command context_path("bin")/"pear", "config-set", key, value, "system"
end
run_command context_path("bin")/"pear", "update-channels"
return if context_name == "php"
ext_config_path = context_path("etc")/"php/#{version_major_minor}/conf.d/ext-opcache.ini"
ext_config_path.dirname.mkpath
zend_extension_line = %Q(zend_extension="#{php_ext_dir}/opcache.so")
if ext_config_path.exist?
require "utils/inreplace"
Utils::Inreplace.inreplace(ext_config_path, /^\s*zend_extension\s*=.*$/, zend_extension_line)
else
ext_config_path.atomic_write <<~INI
[opcache]
#{zend_extension_line}
INI
end
end
end
end
end
@@ -20,7 +20,7 @@ module RuboCop
NOTICE_STEP_METHODS = [:warn].freeze
FORMULA_ACTION_STEP_METHODS =
[:configure_gcc_runtime, :install_gzipped_executable, :configure_glibc_runtime,
:configure_clang_system].freeze
:configure_clang_system, :configure_php].freeze
STEP_SCOPE_METHODS = [:if_path_exists, :unless_path_exists, :on_macos, :on_linux].freeze
ALLOWED_STEP_METHODS = T.let(
[*FILE_PREPARATION_STEP_METHODS, *LINK_STEP_METHODS, *CONFIG_WRITE_STEP_METHODS, *SERVICE_DATA_STEP_METHODS,
@@ -927,6 +927,93 @@ RSpec.describe Homebrew::InstallSteps do
Homebrew::InstallSteps::Runner.new(context: clang_context).run(steps)
end
describe "configures PHP" do
let(:steps) do
Homebrew::InstallSteps::DSL.build do
configure_php
end
end
let(:homebrew_prefix) { root/"homebrew" }
let(:pear_prefix) { root/"prefix/share/php@8.4/pear" }
let(:pecl_path) { homebrew_prefix/"lib/php/pecl" }
let(:ext_config_path) { homebrew_prefix/"etc/php/8.4/conf.d/ext-opcache.ini" }
let(:php_context) do
root_path = root
context.tap do |value|
value.define_singleton_method(:name) { "php@8.4" }
value.define_singleton_method(:version) { Version.new("8.4.1") }
value.define_singleton_method(:pkgshare) { root_path/"prefix/share/php@8.4" }
value.define_singleton_method(:opt_prefix) { root_path/"opt/php@8.4" }
value.define_singleton_method(:etc) { root_path/"homebrew/etc" }
end
end
let(:runner) { Homebrew::InstallSteps::Runner.new(context: php_context) }
before do
stub_const("HOMEBREW_PREFIX", homebrew_prefix)
(pear_prefix/".channels/.alias").mkpath
(pear_prefix/".channels/pear.php.net.reg").write "channel"
(pear_prefix/".channels/.alias/pear.txt").write "alias"
(pear_prefix/".depdblock").write "lock"
FileUtils.chmod 0700, [pear_prefix/".channels", pear_prefix/".channels/.alias"]
FileUtils.chmod 0600, [pear_prefix/".channels/pear.php.net.reg", pear_prefix/".channels/.alias/pear.txt",
pear_prefix/".depdblock"]
(homebrew_prefix/"share").mkpath
File.symlink root/"missing-pecl", root/"prefix/pecl"
allow(runner).to receive(:run_command_output)
.with(root/"prefix/bin/php-config", "--extension-dir")
.and_return("/usr/local/lib/php/20240924\n")
allow(runner).to receive(:run_command)
end
specify "updates PEAR, PECL and opcache configuration", :aggregate_failures do
expect(runner).to receive(:run_command).with(
root/"prefix/bin/pear", "config-set", "ext_dir", pecl_path/"20240924", "system"
).ordered
expect(runner).to receive(:run_command).with(root/"prefix/bin/pear", "update-channels").ordered
runner.run(steps)
expect((pear_prefix/".channels").stat.mode & 0777).to eq(0755)
expect((pear_prefix/".channels/.alias").stat.mode & 0777).to eq(0755)
expect((pear_prefix/".channels/pear.php.net.reg").stat.mode & 0777).to eq(0644)
expect((pear_prefix/".channels/.alias/pear.txt").stat.mode & 0777).to eq(0644)
expect((pear_prefix/".depdblock").stat.mode & 0777).to eq(0644)
expect(root/"prefix/pecl").to be_a_symlink
expect((root/"prefix/pecl").readlink).to eq(pecl_path)
expect(pecl_path/"20240924").to be_a_directory
expect(homebrew_prefix/"share/pear@8.4/.depdblock").to exist
expect(ext_config_path.read).to eq <<~INI
[opcache]
zend_extension="#{root}/opt/php@8.4/lib/php/20240924/opcache.so"
INI
end
specify "only replaces the active opcache extension setting" do
ext_config_path.dirname.mkpath
ext_config_path.write <<~INI
; zend_extension=keep.so
zend_extension = old.so
description=zend_extension=also-keep
INI
runner.run(steps)
expect(ext_config_path.read).to eq <<~INI
; zend_extension=keep.so
zend_extension="#{root}/opt/php@8.4/lib/php/20240924/opcache.so"
description=zend_extension=also-keep
INI
end
specify "audits existing opcache extension settings" do
ext_config_path.dirname.mkpath
ext_config_path.write "[opcache]\n"
expect { runner.run(steps) }.to raise_error(Utils::Inreplace::Error)
end
end
describe "runs gtk_update_icon_cache rebuild action" do
let(:formula) { instance_double(Formula, opt_bin: root/"opt/bin") }
let(:steps) do
@@ -41,7 +41,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
post_install_steps do
system "true"
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `configure_php`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
RUBY
@@ -72,6 +72,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
install_gzipped_executable "compressed.gz", "bin/executable"
configure_glibc_runtime
configure_clang_system
configure_php
write "foo/banner", <<~TEXT
literal banner
TEXT
@@ -107,7 +108,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
post_install_steps do
on_macos do
system "true"
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `configure_php`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
end
@@ -121,7 +122,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
post_install_steps do
write "foo.conf", "prefix = #{prefix}"
^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `configure_php`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
RUBY
+1
View File
@@ -1155,6 +1155,7 @@ Use the named actions below for formula families that share post-install algorit
* `install_gzipped_executable`: unpack and install a gzipped executable.
* `configure_glibc_runtime`: generate requested glibc locales and timezone links.
* `configure_clang_system`: generate macOS Clang system configuration files.
* `configure_php`: configure shared PEAR and PECL state.
#### Service data directory steps