mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
Several binary formulae ship a gzipped executable that needs the same guarded post-install expansion and mode handling. - resolve serialised source and destination path bases - skip absent payloads and replace stale expanded sources - install each executable with an explicit literal permission mode
1053 lines
39 KiB
Ruby
1053 lines
39 KiB
Ruby
# typed: false
|
|
# frozen_string_literal: true
|
|
|
|
require "install_steps"
|
|
require "cask/quarantine"
|
|
|
|
RSpec.describe Homebrew::InstallSteps do
|
|
let(:root) { Pathname(TEST_TMPDIR)/"install-steps" }
|
|
let(:context) do
|
|
root_path = root
|
|
Class.new do
|
|
define_method(:prefix) { root_path/"prefix" }
|
|
define_method(:bin) { root_path/"prefix/bin" }
|
|
define_method(:libexec) { root_path/"prefix/libexec" }
|
|
define_method(:var) { root_path/"var" }
|
|
define_method(:staged_path) { root_path/"stage" }
|
|
end.new
|
|
end
|
|
|
|
before do
|
|
FileUtils.rm_rf root
|
|
end
|
|
|
|
after do
|
|
FileUtils.rm_rf root
|
|
end
|
|
|
|
around do |example|
|
|
with_env(HOMEBREW_GITHUB_ACTIONS: nil) do
|
|
example.run
|
|
end
|
|
end
|
|
|
|
specify "runs mkdir, touch, move and symlink steps", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var, default_source_base: :staged_path,
|
|
default_target_base: :staged_path) do
|
|
mkdir_p "log/example"
|
|
touch "state/marker", base: :prefix
|
|
mv "move-source", "move-target"
|
|
ln_s "move-target", "linked-target", source_base: :relative
|
|
end
|
|
|
|
(root/"stage").mkpath
|
|
(root/"stage/move-source").write "moved"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"var/log/example").to be_a_directory
|
|
expect(root/"prefix/state/marker").to exist
|
|
expect(root/"stage/move-target").to exist
|
|
expect(root/"stage/linked-target").to be_a_symlink
|
|
expect((root/"stage/linked-target").readlink).to eq(Pathname("move-target"))
|
|
end
|
|
|
|
specify "links every source matched by a glob into a directory", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :prefix,
|
|
default_target_base: :prefix) do
|
|
symlink "share/man/*.1", "share/man/man1", force: true, source_glob: true
|
|
end
|
|
|
|
(root/"prefix/share/man/man1").mkpath
|
|
(root/"prefix/share/man/tool.1").write "tool"
|
|
(root/"prefix/share/man/other.1").write "other"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"prefix/share/man/man1/tool.1").to be_a_symlink
|
|
expect(root/"prefix/share/man/man1/other.1").to be_a_symlink
|
|
end
|
|
|
|
specify "runs mkdir without creating parent directories" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
mkdir "missing-parent/example"
|
|
end
|
|
|
|
expect(steps).to include(
|
|
"type" => "mkdir",
|
|
"path" => {
|
|
"base" => "var",
|
|
"path" => "missing-parent/example",
|
|
},
|
|
)
|
|
expect { Homebrew::InstallSteps::Runner.new(context:).run(steps) }.to raise_error(Errno::ENOENT)
|
|
end
|
|
|
|
specify "runs mkdir_p recursively" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
mkdir_p "nested/example"
|
|
end
|
|
|
|
expect(steps).to include(
|
|
"type" => "mkdir_p",
|
|
"path" => {
|
|
"base" => "var",
|
|
"path" => "nested/example",
|
|
},
|
|
)
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"var/nested/example").to be_a_directory
|
|
end
|
|
|
|
specify "normalises API step keys and values" do
|
|
steps = [
|
|
{
|
|
type: :mkdir_p,
|
|
path: {
|
|
base: :var,
|
|
path: "nested/example",
|
|
},
|
|
},
|
|
{
|
|
type: :delete_keychain_certificate,
|
|
name: "NodeMITMProxyCA",
|
|
matching_certificate: "~/Library/Application Support/betwixt/ssl/certs/ca.pem",
|
|
},
|
|
{
|
|
type: :set_permissions,
|
|
paths: ["Example.app"],
|
|
permissions: "0755",
|
|
},
|
|
{
|
|
type: :set_ownership,
|
|
paths: [{ base: :staged_path, path: "Example.app" }],
|
|
user: :root,
|
|
group: :wheel,
|
|
},
|
|
]
|
|
|
|
expect(Homebrew::InstallSteps::DSL.normalise_steps(steps)).to contain_exactly(
|
|
{
|
|
"type" => "mkdir_p",
|
|
"path" => {
|
|
"base" => "var",
|
|
"path" => "nested/example",
|
|
},
|
|
},
|
|
{
|
|
"type" => "delete_keychain_certificate",
|
|
"name" => "NodeMITMProxyCA",
|
|
"matching_certificate" => {
|
|
"path" => "~/Library/Application Support/betwixt/ssl/certs/ca.pem",
|
|
},
|
|
},
|
|
{
|
|
"type" => "set_permissions",
|
|
"paths" => [{ "path" => "Example.app" }],
|
|
"permissions" => "0755",
|
|
},
|
|
{
|
|
"type" => "set_ownership",
|
|
"paths" => [{
|
|
"base" => "staged_path",
|
|
"path" => "Example.app",
|
|
}],
|
|
"user" => "root",
|
|
"group" => "wheel",
|
|
},
|
|
)
|
|
end
|
|
|
|
specify "expands a scoped set of content tokens and leaves others verbatim", :aggregate_failures do
|
|
root_path = root
|
|
versioned_context = Class.new do
|
|
define_method(:prefix) { root_path/"prefix" }
|
|
define_method(:version) { Version.new("1.2.3") }
|
|
end.new
|
|
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :prefix) do
|
|
write "config.ini", <<~EOS
|
|
prefix = {{prefix}}
|
|
cellar = {{HOMEBREW_PREFIX}}
|
|
series = {{version.major_minor}} ({{version}})
|
|
literal = {{unknown}} {single}
|
|
EOS
|
|
end
|
|
|
|
Homebrew::InstallSteps::Runner.new(context: versioned_context).run(steps)
|
|
|
|
written = (root/"prefix/config.ini").read
|
|
expect(written).to include("prefix = #{root}/prefix")
|
|
expect(written).to include("cellar = #{HOMEBREW_PREFIX}")
|
|
expect(written).to include("series = 1.2 (1.2.3)")
|
|
expect(written).to include("literal = {{unknown}} {single}")
|
|
end
|
|
|
|
specify "expands formula and cask identity tokens" do
|
|
context.define_singleton_method(:name) { "example-formula" }
|
|
context.define_singleton_method(:token) { "example-cask" }
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
write "identity", "#{formula_name}:#{token}"
|
|
end
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect((root/"var/identity").read).to eq("example-formula:example-cask\n")
|
|
end
|
|
|
|
specify "writes a default config file and preserves existing ones", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
write "config/new.conf", "fresh"
|
|
write "config/kept.conf", "default"
|
|
write "config/replaced.conf", "default", overwrite: true
|
|
end
|
|
|
|
(root/"var/config").mkpath
|
|
(root/"var/config/kept.conf").write "user edit"
|
|
(root/"var/config/replaced.conf").write "user edit"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect((root/"var/config/new.conf").read).to eq("fresh\n")
|
|
expect((root/"var/config/kept.conf").read).to eq("user edit")
|
|
expect((root/"var/config/replaced.conf").read).to eq("default\n")
|
|
end
|
|
|
|
specify "snapshots conditions shared by a scope" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
unless_path_exists "initialised" do
|
|
mkdir_p "initialised"
|
|
touch "initialised/marker"
|
|
end
|
|
end
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"var/initialised/marker").to exist
|
|
end
|
|
|
|
specify "keeps guard snapshots separate between concatenated step lists" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
if_path_exists "missing" do
|
|
touch "unexpected"
|
|
end
|
|
end
|
|
steps.concat(
|
|
Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
if_path_exists "existing" do
|
|
touch "matched"
|
|
end
|
|
end,
|
|
)
|
|
(root/"var/existing").mkpath
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"var/matched").to exist
|
|
end
|
|
|
|
specify "runs only steps matching the simulated platform" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
on_macos do
|
|
touch "macos"
|
|
end
|
|
on_linux do
|
|
touch "linux"
|
|
end
|
|
end
|
|
|
|
expect([:macos, :linux].to_h do |os|
|
|
FileUtils.rm_rf root/"var"
|
|
Homebrew::SimulateSystem.with(os:) do
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
end
|
|
[os, [(root/"var/macos").exist?, (root/"var/linux").exist?]]
|
|
end).to eq(macos: [true, false], linux: [false, true])
|
|
end
|
|
|
|
specify "copies paths" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var, default_source_base: :staged_path,
|
|
default_target_base: :var) do
|
|
copy "source.txt", "copied.txt"
|
|
end
|
|
|
|
(root/"stage").mkpath
|
|
(root/"stage/source.txt").write "copied"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect((root/"var/copied.txt").read).to eq("copied")
|
|
end
|
|
|
|
specify "replaces copied paths by default and can reject replacement" do
|
|
rejecting_steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path,
|
|
default_target_base: :var) do
|
|
copy "source.txt", "copied.txt", overwrite: false
|
|
end
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :var) do
|
|
copy "source.txt", "copied.txt"
|
|
end
|
|
(root/"stage").mkpath
|
|
(root/"var").mkpath
|
|
(root/"stage/source.txt").write "replacement"
|
|
(root/"var/copied.txt").write "existing"
|
|
|
|
expect { Homebrew::InstallSteps::Runner.new(context:).run(rejecting_steps) }.to raise_error(Errno::EEXIST)
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
expect((root/"var/copied.txt").read).to eq("replacement")
|
|
end
|
|
|
|
specify "replaces symlink destinations when copying files" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :var) do
|
|
copy "source.txt", "copied.txt"
|
|
end
|
|
(root/"stage").mkpath
|
|
(root/"var").mkpath
|
|
(root/"stage/source.txt").write "replacement"
|
|
(root/"var/linked.txt").write "original"
|
|
File.symlink "linked.txt", root/"var/copied.txt"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect([(root/"var/copied.txt").symlink?, (root/"var/copied.txt").read, (root/"var/linked.txt").read])
|
|
.to eq([false, "replacement", "original"])
|
|
end
|
|
|
|
specify "keeps the shipped move replacement default" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :var) do
|
|
move "source.txt", "target.txt"
|
|
end
|
|
(root/"stage").mkpath
|
|
(root/"var").mkpath
|
|
(root/"stage/source.txt").write "replacement"
|
|
(root/"var/target.txt").write "existing"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect((root/"var/target.txt").read).to eq("replacement")
|
|
end
|
|
|
|
specify "requires one match for single-source globs" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :var) do
|
|
copy "*.txt", "copied.txt", source_glob: true
|
|
end
|
|
(root/"stage").mkpath
|
|
(root/"stage/first.txt").write "first"
|
|
(root/"stage/second.txt").write "second"
|
|
|
|
expect { Homebrew::InstallSteps::Runner.new(context:).run(steps) }
|
|
.to raise_error(ArgumentError, /exactly one path/)
|
|
end
|
|
|
|
specify "requires a match for literal glob sources" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :var) do
|
|
copy "missing.txt", "copied.txt", source_glob: true
|
|
end
|
|
|
|
expect { Homebrew::InstallSteps::Runner.new(context:).run(steps) }
|
|
.to raise_error(ArgumentError, /exactly one path/)
|
|
end
|
|
|
|
specify "deduplicates overlapping move source globs" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path,
|
|
default_target_base: :staged_path) do
|
|
move "{WezTerm-*,WezTerm-*}/WezTerm.app", ".", source_glob: true
|
|
end
|
|
(root/"stage/WezTerm-nightly/WezTerm.app").mkpath
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"stage/WezTerm.app").to be_a_directory
|
|
end
|
|
|
|
specify "removes paths and expands globs", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
remove ["obsolete.txt", "obsolete-*"], recursive: true
|
|
end
|
|
|
|
(root/"var/obsolete-dir").mkpath
|
|
(root/"var/obsolete.txt").write "obsolete"
|
|
(root/"var/obsolete-dir/child").write "obsolete"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"var/obsolete.txt").not_to exist
|
|
expect(root/"var/obsolete-dir").not_to exist
|
|
end
|
|
|
|
specify "filters removals by symlink target and file content", :aggregate_failures do
|
|
ENV["PATH"] = (root/"path-bin").to_s
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
remove "links/*", base: :staged_path, symlink_target_contains: "wanted"
|
|
remove "launcher", base: :path, content_contains: "owned marker"
|
|
end
|
|
|
|
(root/"stage/links").mkpath
|
|
(root/"stage/links/wanted").make_symlink("/tmp/wanted-target")
|
|
(root/"stage/links/kept").make_symlink("/tmp/other-target")
|
|
(root/"path-bin").mkpath
|
|
(root/"path-bin/launcher").write "owned marker"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"stage/links/wanted").not_to exist
|
|
expect(root/"stage/links/kept").to be_a_symlink
|
|
expect(root/"path-bin/launcher").not_to exist
|
|
end
|
|
|
|
specify "uses elevated cask removal when requested" do
|
|
require "cask/utils"
|
|
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
remove "protected", sudo: true
|
|
end
|
|
(root/"var/protected").mkpath
|
|
command = class_double(SystemCommand)
|
|
expect(Cask::Utils).to receive(:gain_permissions_remove).with(root/"var/protected", command:)
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps)
|
|
end
|
|
|
|
specify "replaces literal and regular expression matches", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
inreplace "literal.txt", "before", "after"
|
|
inreplace "pattern.txt", /before.+after/i, "replaced"
|
|
end
|
|
|
|
(root/"var").mkpath
|
|
(root/"var/literal.txt").write "before"
|
|
(root/"var/pattern.txt").write "BEFORE and AFTER"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect((root/"var/literal.txt").read).to eq("after")
|
|
expect((root/"var/pattern.txt").read).to eq("replaced")
|
|
end
|
|
|
|
specify "warns inside a matching path scope" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
if_path_exists "{{var}}/{missing,conflict}" do
|
|
warn "{{token}} conflict"
|
|
end
|
|
end
|
|
named_context = context
|
|
named_context.define_singleton_method(:name) { ["Example"] }
|
|
named_context.define_singleton_method(:token) { "example" }
|
|
(root/"var/conflict").mkpath
|
|
runner = Homebrew::InstallSteps::Runner.new(context: named_context)
|
|
expect(runner).to receive(:opoo).with("example conflict")
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "runs serialised commands" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
run "helper", args: ["--path={{var}}"], base: :libexec, env: { "EXAMPLE" => "{{var}}/value" }
|
|
end
|
|
|
|
command = class_double(SystemCommand)
|
|
expect(command).to receive(:run!)
|
|
.with(root/"prefix/libexec/helper", args: ["--path=#{root}/var"], sudo: false,
|
|
env: { "EXAMPLE" => "#{root}/var/value" }, input: [], print_stdout: false,
|
|
print_stderr: true, reset_uid: true, chdir: nil)
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps)
|
|
end
|
|
|
|
specify "serialises command environments as JSON objects" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
run "helper", env: { "EXAMPLE" => "{{formula_name}}" }
|
|
end
|
|
|
|
expect(steps).to include(a_hash_including(
|
|
"type" => "run", "env" => { "EXAMPLE" => "{{formula_name}}" },
|
|
))
|
|
end
|
|
|
|
specify "runs commands with serialised input and output paths" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
run "filter", base: :bin, stdin_path: "input.txt", stdout_path: "output.txt", chdir: "work"
|
|
end
|
|
|
|
(root/"var/work").mkpath
|
|
(root/"var/input.txt").write "input"
|
|
result = instance_double(SystemCommand::Result, stdout: "output")
|
|
command = class_double(SystemCommand)
|
|
expect(command).to receive(:run!)
|
|
.with(root/"prefix/bin/filter", args: [], sudo: false, env: {}, input: "input", print_stdout: false,
|
|
print_stderr: true, reset_uid: true, chdir: root/"var/work")
|
|
.and_return(result)
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps)
|
|
|
|
expect((root/"var/output.txt").read).to eq("output")
|
|
end
|
|
|
|
specify "can ignore process termination failure after retries" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
terminate_process "/Applications/Example.app", match: :full, attempts: 3,
|
|
notices: ["Closing {{name}}"],
|
|
failure_message: "Unable to close {{name}}"
|
|
end
|
|
|
|
named_context = context
|
|
named_context.define_singleton_method(:name) { "Example" }
|
|
command = class_double(SystemCommand)
|
|
runner = Homebrew::InstallSteps::Runner.new(context: named_context, command:)
|
|
allow(runner).to receive(:sleep)
|
|
expect(runner).to receive(:ohai).with("Closing Example")
|
|
expect(runner).to receive(:opoo).with("Unable to close Example")
|
|
expect(command).to receive(:run!)
|
|
.with("/usr/bin/pkill", args: ["-f", "/Applications/Example.app"], sudo: false,
|
|
print_stdout: true, print_stderr: true, reset_uid: true)
|
|
.exactly(3).times
|
|
.and_raise(ErrorDuringExecution.new([], status: 1))
|
|
|
|
expect { runner.run(steps) }.not_to raise_error
|
|
end
|
|
|
|
specify "rejects unknown process match modes" do
|
|
expect do
|
|
Homebrew::InstallSteps::DSL.build do
|
|
terminate_process "Example", match: :prefix
|
|
end
|
|
end.to raise_error(ArgumentError, "terminate_process match must be :name or :full")
|
|
end
|
|
|
|
specify "rejects non-positive process termination attempts" do
|
|
expect do
|
|
Homebrew::InstallSteps::DSL.build do
|
|
terminate_process "Example", attempts: 0
|
|
end
|
|
end.to raise_error(ArgumentError, "terminate_process attempts must be positive")
|
|
end
|
|
|
|
specify "uses killall for exact process names" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
terminate_process "Example", must_succeed: true
|
|
end
|
|
|
|
command = class_double(SystemCommand)
|
|
expect(command).to receive(:run!)
|
|
.with("/usr/bin/killall", args: ["Example"], sudo: false,
|
|
print_stdout: true, print_stderr: true, reset_uid: true)
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps)
|
|
end
|
|
|
|
specify "appends a trailing newline unless already present", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
write "missing-newline", "value"
|
|
write "has-newline", "value\n"
|
|
end
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect((root/"var/missing-newline").read).to eq("value\n")
|
|
expect((root/"var/has-newline").read).to eq("value\n")
|
|
end
|
|
|
|
specify "raises when a write step has missing or blank content" do
|
|
expect do
|
|
Homebrew::InstallSteps::Runner.new(context:).run([{ "type" => "write", "path" => "config/new.conf" }])
|
|
end.to raise_error(ArgumentError, /non-empty content/)
|
|
end
|
|
|
|
specify "runs service data directory initialisers", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
init_data_dir "postgresql@16", using: :postgresql_initdb
|
|
init_data_dir "postgresql@12", using: :postgresql_initdb, locale: "C"
|
|
init_data_dir "mysql", using: :mysql_initialize
|
|
init_data_dir "mysql", using: :mariadb_install_db
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
|
|
expect(runner).to receive(:run_command).with(root/"prefix/bin/initdb", "--locale=en_US.UTF-8", "-E", "UTF-8",
|
|
root/"var/postgresql@16").ordered
|
|
expect(runner).to receive(:run_command).with(root/"prefix/bin/initdb", "--locale=C", "-E", "UTF-8",
|
|
root/"var/postgresql@12").ordered
|
|
expect(runner).to receive(:run_command).with(root/"prefix/bin/mysqld", "--initialize-insecure",
|
|
"--user=#{ENV.fetch("USER")}", "--basedir=#{root}/prefix",
|
|
"--datadir=#{root}/var/mysql", "--tmpdir=/tmp").ordered
|
|
expect(runner).to receive(:run_command).with(root/"prefix/bin/mysql_install_db", "--verbose",
|
|
"--user=#{ENV.fetch("USER")}", "--basedir=#{root}/prefix",
|
|
"--datadir=#{root}/var/mysql", "--tmpdir=/tmp").ordered
|
|
|
|
runner.run(steps)
|
|
|
|
expect(root/"var/postgresql@16").to be_a_directory
|
|
expect(root/"var/postgresql@12").to be_a_directory
|
|
expect(root/"var/mysql").to be_a_directory
|
|
end
|
|
|
|
specify "links remapped directories and children before running initdb", :aggregate_failures do
|
|
homebrew_prefix = root/"homebrew-prefix"
|
|
stub_const("HOMEBREW_PREFIX", homebrew_prefix)
|
|
root_path = root
|
|
versioned_context = Class.new do
|
|
define_method(:name) { "postgresql@17" }
|
|
define_method(:version) { Version.new("17.5") }
|
|
define_method(:prefix) { root_path/"prefix" }
|
|
define_method(:bin) { root_path/"prefix/bin" }
|
|
define_method(:var) { root_path/"var" }
|
|
end.new
|
|
%w[include lib share].each do |dir|
|
|
(root/"prefix/#{dir}/postgresql/server").mkpath
|
|
(root/"prefix/#{dir}/postgresql/server/extension.h").write dir
|
|
(root/"prefix/#{dir}/postgresql/postgres.bki").write dir
|
|
(root/"prefix/#{dir}/postgresql/.DS_Store").write ""
|
|
(homebrew_prefix/dir/"postgresql@17/server").mkpath
|
|
(homebrew_prefix/dir/"postgresql@17/server/local.h").write dir
|
|
end
|
|
(root/"prefix/share/postgresql/conflicting-path").write "source file"
|
|
(homebrew_prefix/"share/postgresql@17/conflicting-path").mkpath
|
|
(homebrew_prefix/"share/postgresql@17/conflicting-path/local").write "kept"
|
|
(root/"prefix/bin").mkpath
|
|
(root/"prefix/bin/initdb").write ""
|
|
FileUtils.chmod "+x", root/"prefix/bin/initdb"
|
|
(root/"prefix/bin/pg_config").write ""
|
|
FileUtils.chmod "+x", root/"prefix/bin/pg_config"
|
|
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var, default_source_base: :prefix) do
|
|
link_dir "include/postgresql", "include/#{name}"
|
|
link_dir "lib/postgresql", "lib/#{name}"
|
|
link_dir "share/postgresql", "share/#{name}"
|
|
link_children "bin", suffix: "-#{version.major}"
|
|
init_data_dir name, using: :postgresql_initdb
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context: versioned_context)
|
|
|
|
expect(runner).to receive(:run_command) do |*args|
|
|
expect(args).to eq([root/"prefix/bin/initdb", "--locale=en_US.UTF-8", "-E", "UTF-8",
|
|
root/"var/postgresql@17"])
|
|
expect(homebrew_prefix/"share/postgresql@17").to be_a_directory
|
|
expect(homebrew_prefix/"share/postgresql@17/postgres.bki").to be_a_symlink
|
|
end
|
|
|
|
runner.run(steps)
|
|
|
|
%w[include lib share].each do |dir|
|
|
expect(homebrew_prefix/dir/"postgresql@17/server").to be_a_directory
|
|
expect(homebrew_prefix/dir/"postgresql@17/server/local.h").to exist
|
|
expect(homebrew_prefix/dir/"postgresql@17/server/extension.h").to be_a_symlink
|
|
expect(homebrew_prefix/dir/"postgresql@17/postgres.bki").to be_a_symlink
|
|
expect(homebrew_prefix/dir/"postgresql@17/.DS_Store").not_to exist
|
|
end
|
|
expect(homebrew_prefix/"share/postgresql@17/conflicting-path/local").to exist
|
|
expect(homebrew_prefix/"bin/initdb-17").to be_a_symlink
|
|
expect(homebrew_prefix/"bin/pg_config-17").to be_a_symlink
|
|
end
|
|
|
|
specify "skips data directory initialisers in CI", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
init_data_dir "postgresql@16", using: :postgresql_initdb
|
|
end
|
|
|
|
ENV["HOMEBREW_GITHUB_ACTIONS"] = "1"
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).not_to receive(:run_command)
|
|
|
|
runner.run(steps)
|
|
|
|
expect(root/"var/postgresql@16").to be_a_directory
|
|
end
|
|
|
|
specify "skips data directory initialisers when their marker exists", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
init_data_dir "mysql", using: :mysql_initialize
|
|
end
|
|
|
|
(root/"var/mysql/mysql").mkpath
|
|
(root/"var/mysql/mysql/general_log.CSM").write ""
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).not_to receive(:run_command)
|
|
|
|
runner.run(steps)
|
|
|
|
expect(root/"var/mysql").to be_a_directory
|
|
end
|
|
|
|
specify "raises on unknown data directory initialisers" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
init_data_dir "unknown", using: :unknown_database
|
|
end
|
|
|
|
ENV["HOMEBREW_GITHUB_ACTIONS"] = "1"
|
|
|
|
expect { Homebrew::InstallSteps::Runner.new(context:).run(steps) }
|
|
.to raise_error(ArgumentError, /unknown data directory initialiser/)
|
|
expect(root/"var/unknown").not_to exist
|
|
end
|
|
|
|
specify "runs named desktop and cache rebuild actions" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
compile_gsettings_schemas
|
|
gio_querymodules
|
|
gdk_pixbuf_query_loaders
|
|
update_mime_database
|
|
update_desktop_database
|
|
end
|
|
|
|
formula = instance_double(Formula, opt_bin: root/"opt/bin")
|
|
allow(Formula).to receive(:[]).with("glib").and_return(formula)
|
|
allow(Formula).to receive(:[]).with("gdk-pixbuf").and_return(formula)
|
|
allow(Formula).to receive(:[]).with("shared-mime-info").and_return(formula)
|
|
allow(Formula).to receive(:[]).with("desktop-file-utils").and_return(formula)
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/glib-compile-schemas",
|
|
HOMEBREW_PREFIX/"share/glib-2.0/schemas").ordered
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/gio-querymodules",
|
|
HOMEBREW_PREFIX/"lib/gio/modules").ordered
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/gdk-pixbuf-query-loaders", "--update-cache").ordered
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/update-mime-database",
|
|
HOMEBREW_PREFIX/"share/mime").ordered
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/update-desktop-database",
|
|
HOMEBREW_PREFIX/"share/applications").ordered
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "dispatches GCC runtime configuration" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
configure_gcc_runtime
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_configure_gcc_runtime)
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "configures GCC runtime files on Linux", :aggregate_failures do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
configure_gcc_runtime
|
|
end
|
|
gcc_context = context
|
|
gcc_context.define_singleton_method(:name) { "gcc" }
|
|
libgcc = root/"gcc/lib/gcc/15"
|
|
crtdir = root/"system/lib"
|
|
libgcc.mkpath
|
|
crtdir.mkpath
|
|
crti = crtdir/"crti.o"
|
|
crti.write "crt"
|
|
specs = libgcc/"specs"
|
|
specs.write "old specs"
|
|
Pathname("#{specs}.orig").write "old original specs"
|
|
gcc = root/"prefix/bin/gcc-15"
|
|
original_specs = "*link:\n+ %o \n"
|
|
|
|
allow(Homebrew::SimulateSystem).to receive(:simulating_or_running_on_linux?).and_return(true)
|
|
allow(Utils::Path).to receive(:formula_any_version_installed?).with("glibc").and_return(false)
|
|
allow(Utils::Path).to receive(:formula_opt_lib).with("glibc").and_return(root/"glibc/lib")
|
|
runner = Homebrew::InstallSteps::Runner.new(context: gcc_context)
|
|
expect(runner).to receive(:context_version_major).and_return("15")
|
|
expect(runner).to receive(:run_command_output)
|
|
.with(gcc, "-print-libgcc-file-name").ordered
|
|
.and_return("#{libgcc}/libgcc_s.so\n")
|
|
expect(runner).to receive(:run_command_output)
|
|
.with("/usr/bin/cc", "-print-file-name=crti.o").ordered
|
|
.and_return("#{crti}\n")
|
|
expect(runner).to receive(:run_command_output)
|
|
.with(gcc, "-print-multiarch").ordered
|
|
.and_return("x86_64-linux-gnu\n")
|
|
expect(runner).to receive(:run_command_output).with(gcc, "-dumpspecs").ordered.and_return(original_specs)
|
|
expect(FileUtils).to receive(:ln_sf).with([crti.to_s], libgcc).and_call_original
|
|
expect(FileUtils).to receive(:rm_f).with(["#{specs}.orig", specs]).and_call_original
|
|
|
|
runner.run(steps)
|
|
|
|
expect(libgcc/"crti.o").to be_a_symlink
|
|
expect((libgcc/"crti.o").readlink).to eq(crti)
|
|
expect(Pathname("#{specs}.orig").read).to eq(original_specs)
|
|
expect(specs.read).to include("%(homebrew_rpath)", "-idirafter /usr/include/x86_64-linux-gnu")
|
|
end
|
|
|
|
specify "dispatches gzipped executable installation" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
install_gzipped_executable "compressed.gz", "bin/executable"
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_install_gzipped_executable)
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "installs a gzipped executable with a fixed mode", :aggregate_failures do
|
|
require "zlib"
|
|
|
|
source = root/"prefix/bin/executable.gz"
|
|
source.dirname.mkpath
|
|
Zlib::GzipWriter.open(source.to_s) do |gzip|
|
|
gzip.orig_name = "stored-name"
|
|
gzip.write "executable"
|
|
end
|
|
stored_name = source.dirname/"stored-name"
|
|
stored_name.write "preserve"
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :prefix, default_target_base: :prefix) do
|
|
install_gzipped_executable "bin/executable.gz", "bin/executable"
|
|
end
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
target = root/"prefix/bin/executable"
|
|
expect(target.read).to eq("executable")
|
|
expect(target.stat.mode & 0777).to eq(0755)
|
|
expect(source).not_to exist
|
|
expect(stored_name.read).to eq("preserve")
|
|
end
|
|
|
|
describe "runs gtk_update_icon_cache rebuild action" do
|
|
let(:formula) { instance_double(Formula, opt_bin: root/"opt/bin") }
|
|
let(:steps) do
|
|
Homebrew::InstallSteps::DSL.build do
|
|
gtk_update_icon_cache
|
|
end
|
|
end
|
|
|
|
it "with gtk4" do
|
|
allow(Formula).to receive(:[]).with("gtk4").and_return(formula)
|
|
allow(Utils::Path).to receive(:formula_any_version_installed?).with("gtk4").and_return(true)
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/gtk4-update-icon-cache", "-q", "-t", "-f",
|
|
HOMEBREW_PREFIX/"share/icons/hicolor").ordered
|
|
runner.run(steps)
|
|
end
|
|
|
|
it "with gtk+3" do
|
|
allow(Formula).to receive(:[]).with("gtk+3").and_return(formula)
|
|
allow(Utils::Path).to receive(:formula_any_version_installed?).with("gtk4").and_return(false)
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_command).with(root/"opt/bin/gtk3-update-icon-cache", "-q", "-t", "-f",
|
|
HOMEBREW_PREFIX/"share/icons/hicolor").ordered
|
|
runner.run(steps)
|
|
end
|
|
end
|
|
|
|
specify "deletes matching keychain certificates by SHA-256 hash" do
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
delete_keychain_certificate "Charles"
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_command_output)
|
|
.with("/usr/bin/security", "find-certificate", "-a", "-c", "Charles", "-Z", sudo: true)
|
|
.and_return(<<~EOS)
|
|
SHA-256 hash: ABC123
|
|
SHA-256 hash: DEF456
|
|
EOS
|
|
expect(runner).to receive(:run_command)
|
|
.with("/usr/bin/security", "delete-certificate", "-Z", "ABC123", sudo: true).ordered
|
|
expect(runner).to receive(:run_command)
|
|
.with("/usr/bin/security", "delete-certificate", "-Z", "DEF456", sudo: true).ordered
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "only deletes the keychain certificate matching a local certificate" do
|
|
certificate = root/"home/Library/Application Support/betwixt/ssl/certs/ca.pem"
|
|
certificate.dirname.mkpath
|
|
certificate.write "certificate"
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
delete_keychain_certificate "NodeMITMProxyCA", matching_certificate: certificate
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).to receive(:run_command_output)
|
|
.with("/usr/bin/openssl", "x509", "-fingerprint", "-sha256", "-noout", "-in", certificate)
|
|
.and_return("sha256 Fingerprint=AB:CD:EF\n")
|
|
expect(runner).to receive(:run_command_output)
|
|
.with("/usr/bin/security", "find-certificate", "-a", "-c", "NodeMITMProxyCA", "-Z", sudo: true)
|
|
.and_return(<<~EOS)
|
|
SHA-256 hash: ABCDEF
|
|
SHA-256 hash: FEDCBA
|
|
EOS
|
|
expect(runner).to receive(:run_command)
|
|
.with("/usr/bin/security", "delete-certificate", "-Z", "ABCDEF", sudo: true)
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "skips keychain certificate deletion when a local certificate is missing" do
|
|
certificate = root/"missing.pem"
|
|
steps = Homebrew::InstallSteps::DSL.build do
|
|
delete_keychain_certificate "NodeMITMProxyCA", matching_certificate: certificate
|
|
end
|
|
|
|
runner = Homebrew::InstallSteps::Runner.new(context:)
|
|
expect(runner).not_to receive(:run_command_output)
|
|
expect(runner).not_to receive(:run_command)
|
|
|
|
runner.run(steps)
|
|
end
|
|
|
|
specify "sets permissions and ownership for existing cask step paths" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :staged_path) do
|
|
set_permissions ["Prepared.app", "Missing.app"], "0755"
|
|
set_permissions "Prepared.file", "0644", recursive: false
|
|
set_ownership "Owned.app", user: "root", group: "wheel"
|
|
set_ownership "Owned.file", user: "root", group: "wheel", recursive: false
|
|
end
|
|
|
|
command = class_double(SystemCommand)
|
|
(root/"stage/Prepared.app").mkpath
|
|
(root/"stage/Prepared.file").write ""
|
|
(root/"stage/Owned.app").mkpath
|
|
(root/"stage/Owned.file").write ""
|
|
|
|
allow(Cask::Quarantine).to receive(:app_management_permissions_granted?)
|
|
.with(app: root/"stage/Owned.app", command:)
|
|
.and_return(true)
|
|
allow(Cask::Quarantine).to receive(:app_management_permissions_granted?)
|
|
.with(app: root/"stage/Owned.file", command:)
|
|
.and_return(true)
|
|
expect(command).to receive(:run!)
|
|
.with("chmod", args: ["-R", "--", "0755", root/"stage/Prepared.app"], sudo: false).ordered
|
|
expect(command).to receive(:run!)
|
|
.with("chmod", args: ["--", "0644", root/"stage/Prepared.file"], sudo: false).ordered
|
|
expect(command).to receive(:run!)
|
|
.with("chown", args: ["-R", "--", "root:wheel", root/"stage/Owned.app"], sudo: true).ordered
|
|
expect(command).to receive(:run!)
|
|
.with("chown", args: ["--", "root:wheel", root/"stage/Owned.file"], sudo: true).ordered
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps)
|
|
end
|
|
|
|
specify "raises when App Management permissions are missing for ownership steps" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :staged_path) do
|
|
set_ownership "Owned.app"
|
|
end
|
|
|
|
command = class_double(SystemCommand)
|
|
(root/"stage/Owned.app").mkpath
|
|
|
|
allow(Cask::Quarantine).to receive(:app_management_permissions_granted?)
|
|
.with(app: root/"stage/Owned.app", command:)
|
|
.and_return(false)
|
|
expect(command).not_to receive(:run!)
|
|
|
|
expect { Homebrew::InstallSteps::Runner.new(context:, command:).run(steps) }
|
|
.to raise_error(Cask::CaskError, /App Management permissions/)
|
|
end
|
|
|
|
specify "does not add the default base to home paths" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
mkdir_p "~/example"
|
|
end
|
|
|
|
expect(steps).to contain_exactly(
|
|
"type" => "mkdir_p",
|
|
"path" => {
|
|
"path" => "~/example",
|
|
},
|
|
)
|
|
end
|
|
|
|
specify "moves a directory's children without moving the new target directory" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :staged_path) do
|
|
move_children ".", "Nested"
|
|
end
|
|
|
|
(root/"stage").mkpath
|
|
(root/"stage/source-file").write "source"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"stage/Nested/source-file").to exist
|
|
end
|
|
|
|
specify "moves a directory's contents" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :staged_path, default_target_base: :staged_path) do
|
|
move_contents ".", "Nested"
|
|
end
|
|
(root/"stage").mkpath
|
|
(root/"stage/source-file").write "source"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps)
|
|
|
|
expect(root/"stage/Nested/source-file").to exist
|
|
end
|
|
|
|
specify "uses sudo only when an if-needed symlink target is not writable" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_target_base: :staged_path) do
|
|
symlink "target", "writable/linked-target", source_base: :relative, sudo: :if_needed
|
|
symlink "target", "protected/linked-target", source_base: :relative, sudo: :if_needed
|
|
end
|
|
protected_dir = root/"stage/protected"
|
|
(root/"stage/writable").mkpath
|
|
protected_dir.mkpath
|
|
FileUtils.chmod "-w", protected_dir
|
|
command = class_double(SystemCommand)
|
|
expect(command).to receive(:run!)
|
|
.with("/bin/ln", args: ["-s", "target", protected_dir/"linked-target"], sudo: true)
|
|
|
|
begin
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps)
|
|
ensure
|
|
FileUtils.chmod "+w", protected_dir
|
|
end
|
|
|
|
expect(root/"stage/writable/linked-target").to be_a_symlink
|
|
end
|
|
|
|
specify "removes symlinks marked for uninstall" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_target_base: :staged_path) do
|
|
ln_sf "target", "linked-target", source_base: :relative, uninstall: true
|
|
end
|
|
|
|
(root/"stage").mkpath
|
|
File.symlink "target", root/"stage/linked-target"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps, phase: :uninstall)
|
|
|
|
expect(root/"stage/linked-target").not_to be_a_symlink
|
|
end
|
|
|
|
specify "preserves symlinks with unexpected targets on uninstall" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_target_base: :staged_path) do
|
|
symlink "target", "linked-target", source_base: :relative, uninstall: true
|
|
end
|
|
(root/"stage").mkpath
|
|
File.symlink "different-target", root/"stage/linked-target"
|
|
|
|
Homebrew::InstallSteps::Runner.new(context:).run(steps, phase: :uninstall)
|
|
|
|
expect(root/"stage/linked-target").to be_a_symlink
|
|
end
|
|
|
|
specify "uses elevated removal for matching symlinks when needed" do
|
|
steps = Homebrew::InstallSteps::DSL.build(default_target_base: :staged_path) do
|
|
symlink "target", "protected/linked-target", source_base: :relative, uninstall: true, sudo: :if_needed
|
|
end
|
|
protected_dir = root/"stage/protected"
|
|
protected_dir.mkpath
|
|
target = protected_dir/"linked-target"
|
|
File.symlink "target", target
|
|
FileUtils.chmod "-w", protected_dir
|
|
command = class_double(SystemCommand)
|
|
expect(Cask::Utils).to receive(:gain_permissions_remove).with(target, command:)
|
|
|
|
begin
|
|
Homebrew::InstallSteps::Runner.new(context:, command:).run(steps, phase: :uninstall)
|
|
ensure
|
|
FileUtils.chmod "+w", protected_dir
|
|
end
|
|
end
|
|
|
|
specify "does not expose the surrounding formula or cask DSL" do
|
|
expect do
|
|
Homebrew::InstallSteps::DSL.build(default_base: :var) do
|
|
system "true"
|
|
end
|
|
end.to raise_error(NameError)
|
|
end
|
|
end
|