From 763531e821ba7ded789ba3b3880ffea1199e93f8 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Mon, 25 Aug 2025 04:08:42 -0400 Subject: [PATCH] Extract `Pathname` refinement from `Formulary` --- Library/Homebrew/build.rb | 4 ++ .../extend/pathname/write_mkpath_extension.rb | 34 ++++++++++ Library/Homebrew/formulary.rb | 20 +----- .../pathname/write_mkpath_extension_spec.rb | 62 +++++++++++++++++++ 4 files changed, 101 insertions(+), 19 deletions(-) create mode 100644 Library/Homebrew/extend/pathname/write_mkpath_extension.rb create mode 100644 Library/Homebrew/test/extend/pathname/write_mkpath_extension_spec.rb diff --git a/Library/Homebrew/build.rb b/Library/Homebrew/build.rb index 69ba43e9de..75e9305b61 100644 --- a/Library/Homebrew/build.rb +++ b/Library/Homebrew/build.rb @@ -17,6 +17,7 @@ require "utils/socket" require "cmd/install" require "json/add/exception" require "utils/output" +require "extend/pathname/write_mkpath_extension" # A formula build. class Build @@ -245,6 +246,9 @@ begin formula = args.named.to_formulae.first options = Options.create(args.flags_only) build = Build.new(formula, options, args:) + + Pathname.prepend WriteMkpathExtension + build.install # Any exception means the build did not complete. # The `case` for what to do per-exception class is further down. diff --git a/Library/Homebrew/extend/pathname/write_mkpath_extension.rb b/Library/Homebrew/extend/pathname/write_mkpath_extension.rb new file mode 100644 index 0000000000..1281b3eef8 --- /dev/null +++ b/Library/Homebrew/extend/pathname/write_mkpath_extension.rb @@ -0,0 +1,34 @@ +# typed: strict +# frozen_string_literal: true + +module WriteMkpathExtension + extend T::Helpers + + requires_ancestor { Pathname } + + # Source for `sig`: https://github.com/sorbet/sorbet/blob/b4092efe0a4489c28aff7e1ead6ee8a0179dc8b3/rbi/stdlib/pathname.rbi#L1392-L1411 + sig { + params( + content: Object, + offset: Integer, + external_encoding: T.any(String, Encoding), + internal_encoding: T.any(String, Encoding), + encoding: T.any(String, Encoding), + textmode: BasicObject, + binmode: BasicObject, + autoclose: BasicObject, + mode: String, + perm: Integer, + ).returns(Integer) + } + def write(content, offset = T.unsafe(nil), external_encoding: T.unsafe(nil), internal_encoding: T.unsafe(nil), + encoding: T.unsafe(nil), textmode: T.unsafe(nil), binmode: T.unsafe(nil), autoclose: T.unsafe(nil), + mode: T.unsafe(nil), perm: T.unsafe(nil)) + T.bind(self, Pathname) + raise "Will not overwrite #{self}" if exist? && !offset && !mode&.match?(/^a\+?$/) + + dirname.mkpath + + super + end +end diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 711c319f46..4c4235d38d 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -1,4 +1,4 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true require "digest/sha2" @@ -112,24 +112,6 @@ module Formulary super end - module PathnameWriteMkpath - # TODO: migrate away from refinements here, they don't play nicely with Sorbet - # rubocop:todo Sorbet/BlockMethodDefinition - refine Pathname do - def write(content, offset = nil, **open_args) - T.bind(self, Pathname) - raise "Will not overwrite #{self}" if exist? && !offset && !open_args[:mode]&.match?(/^a\+?$/) - - dirname.mkpath - - super - end - end - # rubocop:enable Sorbet/BlockMethodDefinition - end - - using PathnameWriteMkpath - sig { params( name: String, diff --git a/Library/Homebrew/test/extend/pathname/write_mkpath_extension_spec.rb b/Library/Homebrew/test/extend/pathname/write_mkpath_extension_spec.rb new file mode 100644 index 0000000000..2e00762a87 --- /dev/null +++ b/Library/Homebrew/test/extend/pathname/write_mkpath_extension_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "extend/pathname/write_mkpath_extension" + +RSpec.describe WriteMkpathExtension do + let(:file_content) { "sample contents" } + + before do + Pathname.prepend described_class + end + + it "creates parent directories if they do not exist" do + mktmpdir do |tmpdir| + file = tmpdir/"foo/bar/baz.txt" + expect(file.dirname).not_to exist + file.write(file_content) + expect(file).to exist + expect(file.read).to eq(file_content) + end + end + + it "raises if file exists and not in append mode or with offset" do + mktmpdir do |tmpdir| + file = tmpdir/"file.txt" + file.write(file_content) + expect { file.write("new content") }.to raise_error(RuntimeError, /Will not overwrite/) + end + end + + it "allows overwrite if offset is provided" do + mktmpdir do |tmpdir| + file = tmpdir/"file.txt" + file.write(file_content) + expect do + file.write("change", 0) + end.not_to raise_error + expect(file.read).to eq("change contents") + end + end + + it "allows append mode ('a')" do + mktmpdir do |tmpdir| + file = tmpdir/"file.txt" + file.write(file_content) + expect do + file.write(" appended", mode: "a") + end.not_to raise_error + expect(file.read).to eq("#{file_content} appended") + end + end + + it "allows append mode ('a+')" do + mktmpdir do |tmpdir| + file = tmpdir/"file.txt" + file.write(file_content) + expect do + file.write(" again", mode: "a+") + end.not_to raise_error + expect(file.read).to include("again") + end + end +end