diff --git a/Library/Homebrew/cask/artifact/abstract_artifact.rb b/Library/Homebrew/cask/artifact/abstract_artifact.rb index 7d5b1d3cc6..9f5247d20d 100644 --- a/Library/Homebrew/cask/artifact/abstract_artifact.rb +++ b/Library/Homebrew/cask/artifact/abstract_artifact.rb @@ -201,13 +201,13 @@ module Cask cask.config end - sig { returns(T.nilable(Sandbox)) } - def cask_sandbox + sig { params(network_access_allowed: T::Boolean).returns(T.nilable(Sandbox)) } + def cask_sandbox(network_access_allowed: false) return unless Sandbox.use_for?("running cask artifact operations") Sandbox.new.tap do |sandbox| sandbox.allow_read(path: cask.staged_path, type: :subpath) - sandbox.add_install_hook_rules(network_access_allowed: false) + sandbox.add_install_hook_rules(network_access_allowed:) end end diff --git a/Library/Homebrew/cask/artifact/install_steps.rb b/Library/Homebrew/cask/artifact/install_steps.rb index 04d623a163..fe5c1c1967 100644 --- a/Library/Homebrew/cask/artifact/install_steps.rb +++ b/Library/Homebrew/cask/artifact/install_steps.rb @@ -33,7 +33,9 @@ module Cask sig { params(command: T.class_of(SystemCommand), phase: Symbol).void } def run_steps(command, phase: :install) runner = Homebrew::InstallSteps::Runner.new(context: cask, command:) - sandbox = cask_sandbox + sandbox = cask_sandbox(network_access_allowed: steps.any? do |step| + step["type"] == "run" && step["network_access"] == true + end) unless sandbox runner.run(steps, phase:) return diff --git a/Library/Homebrew/install_steps.rb b/Library/Homebrew/install_steps.rb index 9274e001f8..60efe3fc32 100644 --- a/Library/Homebrew/install_steps.rb +++ b/Library/Homebrew/install_steps.rb @@ -683,11 +683,12 @@ module Homebrew chdir: ::T.nilable(::T.any(::String, ::Pathname)), writable_paths: Paths, writable_base: ::T.nilable(::T.any(::String, ::Symbol)), + network_access: ::T::Boolean, ).void } def run(command, args: [], base: nil, env: {}, sudo: false, must_succeed: true, print_stdout: false, print_stderr: true, stdin_path: nil, stdout_path: nil, chdir: nil, writable_paths: [], - writable_base: nil) + writable_base: nil, network_access: false) add_step("run", "command" => path_spec(command, base:, default_base: nil), "args" => args.map(&:to_s), @@ -703,7 +704,8 @@ module Homebrew writable_paths, base: writable_base, default_base: @default_base, - )) + ), + "network_access" => network_access) end sig { diff --git a/Library/Homebrew/test/cask/artifact/install_steps_spec.rb b/Library/Homebrew/test/cask/artifact/install_steps_spec.rb index 0729f1a926..9f66aae0f9 100644 --- a/Library/Homebrew/test/cask/artifact/install_steps_spec.rb +++ b/Library/Homebrew/test/cask/artifact/install_steps_spec.rb @@ -64,7 +64,7 @@ RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do artifact = cask.artifacts.find { |candidate| candidate.is_a?(Cask::Artifact::PostflightSteps) } run_step = artifact.steps.find { |step| step["type"] == "run" } - expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths") + expect(run_step).not_to include("print_stdout", "suppress_stderr", "writable_paths", "network_access") end it "sandboxes complete step blocks, including system commands" do @@ -119,6 +119,28 @@ RSpec.describe Cask::Artifact::AbstractInstallSteps, :cask do expect(original_home/"Library/Application Support/cask-home-state").to exist end + it "allows network access for runs that request it" do + cask = Cask::Cask.new("with-networked-install-step") do + version "1.2.3" + sha256 :no_check + url "file://#{TEST_FIXTURE_DIR}/cask/container.zip" + + postflight_steps do + run "/usr/bin/true", network_access: true + end + end + sandbox = instance_double(Sandbox).as_null_object + cask.staged_path.mkpath + cask.config_path.dirname.mkpath + + allow(Sandbox).to receive_messages(available?: true, new: sandbox) + allow(Sandbox).to receive(:with_preserved_brew_file).and_yield + expect(sandbox).to receive(:add_install_hook_rules).with(network_access_allowed: true) + expect(sandbox).to receive(:run) + + Cask::Installer.new(cask, command: NeverSudoSystemCommand).install_artifacts + end + context "when install steps may require sudo" do { "an explicitly privileged command" => proc { run "/usr/bin/true", sudo: true }, diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index 7d23ce2052..fe30a89b34 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -659,7 +659,7 @@ Relative paths default to `staged_path` for `base:`, `source_base:` and `target_ Use `if_path_exists`, `unless_path_exists`, `on_macos` and `on_linux` blocks to guard one or more steps. A condition is evaluated once when its scope begins, so related steps make the same decision. Use `unless_path_exists` around `write_file` when an existing file must be preserved. -`run` does not evaluate a shell command string. It supports a literal `env:`, `stdin_path:`, `stdout_path:`, `chdir:` and `sudo:`. Standard output is hidden by default and standard error is printed; use `print_stdout: true` or `print_stderr: false` to change that behaviour. Failure aborts the installation or uninstallation; pass `must_succeed: false` when a non-zero exit status is expected and should be ignored, such as a cleanup command that cannot run when its dependency is absent. Nothing is written to `stdout_path:` when an ignored command fails. The complete steps block runs in an isolated Ruby subprocess without network or general home-directory access where Homebrew has an available sandbox. The sandbox permits writes to declared step destinations, the caskroom, app directory, temporary and cache directories and Homebrew's link directories. Ruby file operations and system or cask-provided commands therefore share the same restrictions. Use `writable_paths:` with directory roots, and `writable_base:` for relative roots, when an opaque command needs another declared write location. +`run` does not evaluate a shell command string. It supports a literal `env:`, `stdin_path:`, `stdout_path:`, `chdir:` and `sudo:`. Standard output is hidden by default and standard error is printed; use `print_stdout: true` or `print_stderr: false` to change that behaviour. Failure aborts the installation or uninstallation; pass `must_succeed: false` when a non-zero exit status is expected and should be ignored, such as a cleanup command that cannot run when its dependency is absent. Nothing is written to `stdout_path:` when an ignored command fails. The complete steps block runs in an isolated Ruby subprocess without network or general home-directory access where Homebrew has an available sandbox. Pass `network_access: true` when a `run` command must access the network. The sandbox permits writes to declared step destinations, the caskroom, app directory, temporary and cache directories and Homebrew's link directories. Ruby file operations and system or cask-provided commands therefore share the same restrictions. Use `writable_paths:` with directory roots, and `writable_base:` for relative roots, when an opaque command needs another declared write location. #### Interpolation in steps blocks