diff --git a/Library/Homebrew/env_config.rb b/Library/Homebrew/env_config.rb index 84fed24b19..d1cfb673df 100644 --- a/Library/Homebrew/env_config.rb +++ b/Library/Homebrew/env_config.rb @@ -613,7 +613,6 @@ module Homebrew boolean: :set, }, HOMEBREW_NO_SANDBOX_CASK: { - # odeprecated: make cask executable sandboxing mandatory in a future release. description: "If set, disable sandboxing for cask artifacts that generate files by running " \ "executables.", boolean: true, diff --git a/Library/Homebrew/install_steps.rb b/Library/Homebrew/install_steps.rb index 3f25067968..1210e24760 100644 --- a/Library/Homebrew/install_steps.rb +++ b/Library/Homebrew/install_steps.rb @@ -509,16 +509,19 @@ module Homebrew sig { params( - path: ::T.any(::String, ::Pathname), - content: ::String, - base: ::T.nilable(::T.any(::String, ::Symbol)), + path: ::T.any(::String, ::Pathname), + content: ::String, + base: ::T.nilable(::T.any(::String, ::Symbol)), + overwrite: ::T::Boolean, + append_newline: ::T::Boolean, ).void } - def write_file(path, content, base: nil) + def write_file(path, content, base: nil, overwrite: true, append_newline: false) + content = "#{content}\n" if append_newline && !content.end_with?("\n") add_step("write", "path" => path_spec(path, base:, default_base: @default_base), "content" => content, - "overwrite" => true) + "overwrite" => (true if overwrite)) end sig { @@ -553,6 +556,11 @@ module Homebrew add_rebuild_action("gio_querymodules", "lib/gio/modules") end + sig { void } + def update_gio_modules_cache + add_rebuild_action("gio_querymodules", "lib/gio/modules") + end + # odeprecated sig { void } def gdk_pixbuf_query_loaders diff --git a/Library/Homebrew/rubocops/cask/install_steps.rb b/Library/Homebrew/rubocops/cask/install_steps.rb index e86d825c73..e184b90469 100644 --- a/Library/Homebrew/rubocops/cask/install_steps.rb +++ b/Library/Homebrew/rubocops/cask/install_steps.rb @@ -76,6 +76,7 @@ module RuboCop next unless stanza.method_node.block_type? block_node = T.cast(stanza.method_node, RuboCop::AST::BlockNode) + add_compatibility_step_offenses(block_node, allowed_methods: CASK_ALLOWED_STEP_METHODS) if (offense_node = brew_ruby_step_node(block_node)) add_offense(offense_node, message: BREW_RUBY_STEP_MSG) next diff --git a/Library/Homebrew/rubocops/install_steps.rb b/Library/Homebrew/rubocops/install_steps.rb index ae5d877c67..be8b6e1c6e 100644 --- a/Library/Homebrew/rubocops/install_steps.rb +++ b/Library/Homebrew/rubocops/install_steps.rb @@ -12,9 +12,7 @@ module RuboCop extend AutoCorrector include InstallStepsHelper - # TODO: Re-enable when formula `post_install` and `post_install_steps` - # cannot coexist after the incremental conversion bridge is removed. - # CONFLICT_MSG = "`post_install` and `post_install_steps` cannot both be used." + CONFLICT_MSG = "`post_install` and `post_install_steps` cannot both be used." LEGACY_POST_INSTALL_MSG = "Formulae in official Homebrew taps must use `post_install_steps` instead of `post_install`." REDUNDANT_SERVICE_PATH_DIRS_MSG = "`%s` only creates directories created by `brew services`." @@ -91,13 +89,10 @@ module RuboCop post_install_steps_block = find_block(body_node, :post_install_steps) post_install_method = find_method_def(body_node, :post_install) - # TODO: Re-enable when formula `post_install` and - # `post_install_steps` cannot coexist after the incremental - # conversion bridge is removed. - # if post_install_steps_block && post_install_method - # offending_node(post_install_steps_block) - # problem CONFLICT_MSG - # end + if post_install_steps_block && post_install_method + offending_node(post_install_steps_block) + problem CONFLICT_MSG + end redundant_post_install_steps = post_install_steps_block.present? && redundant_service_path_dirs_block?(post_install_steps_block, @@ -126,6 +121,8 @@ module RuboCop def audit_step_block(block_node) return if block_node.nil? + add_compatibility_step_offenses(block_node) + if (offense_node = brew_ruby_step_node(block_node)) offending_node(offense_node) problem BREW_RUBY_STEP_MSG diff --git a/Library/Homebrew/rubocops/shared/install_steps_helper.rb b/Library/Homebrew/rubocops/shared/install_steps_helper.rb index 2e94fc991d..32c072c581 100644 --- a/Library/Homebrew/rubocops/shared/install_steps_helper.rb +++ b/Library/Homebrew/rubocops/shared/install_steps_helper.rb @@ -13,8 +13,8 @@ module RuboCop CONFIG_WRITE_STEP_METHODS = [:write, :write_file].freeze SERVICE_DATA_STEP_METHODS = [:init_data_dir].freeze REBUILD_ACTION_STEP_METHODS = - [:compile_gsettings_schemas, :gio_querymodules, :gdk_pixbuf_query_loaders, :update_gdk_pixbuf_loaders_cache, - :gtk_update_icon_cache, :update_gtk_icon_cache, + [:compile_gsettings_schemas, :gio_querymodules, :update_gio_modules_cache, :gdk_pixbuf_query_loaders, + :update_gdk_pixbuf_loaders_cache, :gtk_update_icon_cache, :update_gtk_icon_cache, :update_mime_database, :update_desktop_database].freeze KEYCHAIN_STEP_METHODS = [:delete_keychain_certificate, :delete_keychain_certificates].freeze PERMISSION_STEP_METHODS = [:set_permissions, :set_ownership].freeze @@ -25,10 +25,36 @@ module RuboCop [:configure_gcc_runtime, :install_gzipped_executable, :configure_glibc_runtime, :configure_clang_system, :configure_php, :bootstrap_cpython, :bootstrap_pypy].freeze STEP_SCOPE_METHODS = [:if_path_exists, :unless_path_exists, :on_macos, :on_linux].freeze - # odeprecated - COMPATIBILITY_STEP_METHODS = - [:mkdir, :mv, :move_children, :ln_s, :ln_sf, :link_dir, :link_children, :write, :gio_querymodules, - :gdk_pixbuf_query_loaders, :gtk_update_icon_cache, :delete_keychain_certificate].freeze + COMPATIBILITY_STEP_METHOD_REPLACEMENTS = T.let( + { + mkdir: :mkdir_p, + mv: :move, + move_children: :move_contents, + ln_s: :symlink, + ln_sf: :symlink, + link_dir: :symlink_tree, + link_children: :symlink_children, + write: :write_file, + gio_querymodules: :update_gio_modules_cache, + gdk_pixbuf_query_loaders: :update_gdk_pixbuf_loaders_cache, + gtk_update_icon_cache: :update_gtk_icon_cache, + delete_keychain_certificate: :delete_keychain_certificates, + }.freeze, + T::Hash[Symbol, Symbol], + ) + COMPATIBILITY_STEP_METHODS = T.let(COMPATIBILITY_STEP_METHOD_REPLACEMENTS.keys.freeze, T::Array[Symbol]) + COMPATIBILITY_STEP_KEYWORD_REPLACEMENTS = T.let( + { + move: { force: :overwrite }.freeze, + mv: { force: :overwrite }.freeze, + symlink: { force: :overwrite, uninstall: :remove_on_uninstall }.freeze, + ln_s: { force: :overwrite, uninstall: :remove_on_uninstall }.freeze, + ln_sf: { uninstall: :remove_on_uninstall }.freeze, + delete_keychain_certificate: { matching_certificate: :fingerprint_of }.freeze, + delete_keychain_certificates: { matching_certificate: :fingerprint_of }.freeze, + }.freeze, + T::Hash[Symbol, T::Hash[Symbol, Symbol]], + ) ALLOWED_STEP_METHODS = T.let( [*FILE_PREPARATION_STEP_METHODS, *LINK_STEP_METHODS, *CONFIG_WRITE_STEP_METHODS, *SERVICE_DATA_STEP_METHODS, *REBUILD_ACTION_STEP_METHODS, :set_permissions, *COMMAND_STEP_METHODS, *NOTICE_STEP_METHODS, @@ -52,6 +78,8 @@ module RuboCop String, ) BREW_RUBY_STEP_MSG = "Install steps must not use `brew ruby` because it enables developer mode." + LEGACY_STEP_METHOD_MSG = "Use `%s` instead of legacy install step `%s`." + LEGACY_STEP_KEYWORD_MSG = "Use `%s:` instead of legacy install step keyword `%s:`." SIMPLE_STEP_CONVERSION_MSG = "Use `%s` for simple file preparation." REBUILD_ACTION_STEP_LINES = T.let( T.let([ @@ -95,6 +123,29 @@ module RuboCop "#{(allowed_methods - COMPATIBILITY_STEP_METHODS).map { |method| "`#{method}`" }.join(", ")}." end + sig { + params( + block_node: RuboCop::AST::BlockNode, + allowed_methods: T::Array[Symbol], + ).void + } + def add_compatibility_step_offenses(block_node, allowed_methods: ALLOWED_STEP_METHODS) + block_node.each_descendant(:send) do |node| + send_node = T.cast(node, RuboCop::AST::SendNode) + method = send_node.method_name + next if send_node.receiver || !allowed_methods.include?(method) + + if (replacement = COMPATIBILITY_STEP_METHOD_REPLACEMENTS[method]) + add_offense(send_node.loc.selector, + message: Kernel.format(LEGACY_STEP_METHOD_MSG, method:, replacement:)) do |corrector| + corrector.replace(send_node.loc.selector, replacement.to_s) + add_compatibility_step_method_corrections(corrector, send_node) + end + end + add_compatibility_step_keyword_offenses(send_node) + end + end + class InstallStepPath < T::Struct const :path, String const :base, T.nilable(Symbol) @@ -211,6 +262,114 @@ module RuboCop private + sig { params(send_node: RuboCop::AST::SendNode).void } + def add_compatibility_step_keyword_offenses(send_node) + replacements = COMPATIBILITY_STEP_KEYWORD_REPLACEMENTS[send_node.method_name] + return if replacements.nil? + + options = send_node.last_argument + return unless options&.hash_type? + + options = T.cast(options, RuboCop::AST::HashNode) + options.pairs.each do |pair| + next unless pair.key.sym_type? + next unless (replacement = replacements[pair.key.value]) + + message = Kernel.format(LEGACY_STEP_KEYWORD_MSG, keyword: pair.key.value, replacement:) + add_offense(pair.key.source_range, message:) do |corrector| + correct_compatibility_step_keyword(corrector, send_node, options, pair, replacement) + end + end + end + + sig { + params( + corrector: RuboCop::Cop::Corrector, + send_node: RuboCop::AST::SendNode, + options: RuboCop::AST::HashNode, + legacy_pair: RuboCop::AST::PairNode, + replacement: Symbol, + ).void + } + def correct_compatibility_step_keyword(corrector, send_node, options, legacy_pair, replacement) + if replacement == :fingerprint_of + corrector.replace(legacy_pair.key.source_range, replacement.to_s) + return + end + return if !legacy_pair.value.true_type? && !legacy_pair.value.false_type? + + canonical_pair = options.pairs.find do |pair| + pair != legacy_pair && pair.key.sym_type? && pair.key.value == replacement + end + if legacy_pair.value.true_type? && canonical_pair.nil? + corrector.replace(legacy_pair.key.source_range, replacement.to_s) + return + end + + if legacy_pair.value.true_type? && canonical_pair && !canonical_pair.value.true_type? + corrector.replace(canonical_pair.value.source_range, "true") + end + pairs = options.pairs + index = pairs.index(legacy_pair) + return if index.nil? + + range = if (next_pair = pairs[index + 1]) + legacy_pair.source_range.begin.join(next_pair.source_range.begin) + elsif (previous_pair = pairs[index - 1]) && index.positive? + previous_pair.source_range.end.join(legacy_pair.source_range.end) + else + send_node.arguments.fetch(-2).source_range.end.join(legacy_pair.source_range.end) + end + corrector.remove(range) + end + + sig { + params( + corrector: RuboCop::Cop::Corrector, + send_node: RuboCop::AST::SendNode, + ).void + } + def add_compatibility_step_method_corrections(corrector, send_node) + case send_node.method_name + when :ln_sf + add_step_keyword(corrector, send_node, "overwrite: true") + when :write + options = send_node.last_argument + overwrite = options&.hash_type? && T.cast(options, RuboCop::AST::HashNode).pairs.any? do |pair| + pair.key.sym_type? && pair.key.value == :overwrite + end + keyword = "append_newline: true" + keyword = "overwrite: false, #{keyword}" unless overwrite + add_step_keyword(corrector, send_node, keyword) + end + end + + sig { + params( + corrector: RuboCop::Cop::Corrector, + send_node: RuboCop::AST::SendNode, + keyword: String, + ).void + } + def add_step_keyword(corrector, send_node, keyword) + options = send_node.last_argument + if options&.hash_type? + options = T.cast(options, RuboCop::AST::HashNode) + if (pair = options.pairs.last) + corrector.insert_after(pair.source_range, ", #{keyword}") + else + corrector.replace(options, keyword) + end + elsif (argument = send_node.last_argument) + range = if argument.loc.respond_to?(:heredoc_end) && argument.loc.heredoc_end + argument.loc.expression + else + argument.source_range + end + corrector.insert_after(range, ", #{keyword}") + end + end + sig { params( node: RuboCop::AST::Node, diff --git a/Library/Homebrew/rubocops/shared/install_steps_helper.rbi b/Library/Homebrew/rubocops/shared/install_steps_helper.rbi new file mode 100644 index 0000000000..951e6638d4 --- /dev/null +++ b/Library/Homebrew/rubocops/shared/install_steps_helper.rbi @@ -0,0 +1,5 @@ +# typed: strict + +module RuboCop::Cop::InstallStepsHelper + requires_ancestor { RuboCop::Cop::Base } +end diff --git a/Library/Homebrew/test/install_steps_spec.rb b/Library/Homebrew/test/install_steps_spec.rb index d87ffdf53b..a70b8190db 100644 --- a/Library/Homebrew/test/install_steps_spec.rb +++ b/Library/Homebrew/test/install_steps_spec.rb @@ -349,6 +349,20 @@ RSpec.describe Homebrew::InstallSteps do expect([(root/"var/config/example.conf").read, (root/"var/empty").read]).to eq(["replacement", ""]) end + specify "can append a newline without replacing existing files" do + steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do + write_file "existing", "replacement", overwrite: false, append_newline: true + write_file "missing", "new", overwrite: false, append_newline: true + end + + (root/"var").mkpath + (root/"var/existing").write "original\n" + + Homebrew::InstallSteps::Runner.new(context:).run(steps) + + expect([(root/"var/existing").read, (root/"var/missing").read]).to eq(["original\n", "new\n"]) + end + specify "preserves meaningful blank values" do steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do inreplace "remove.txt", "remove", "" @@ -904,7 +918,7 @@ RSpec.describe Homebrew::InstallSteps do end steps = Homebrew::InstallSteps::DSL.build do compile_gsettings_schemas - gio_querymodules + update_gio_modules_cache update_gdk_pixbuf_loaders_cache update_mime_database update_desktop_database diff --git a/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb b/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb index 9fe81fdcc1..4c19cb9beb 100644 --- a/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb +++ b/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb @@ -92,8 +92,6 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do touch "foo/state" touch "#{token}/state" move "source", "target" - mv "source", "target" - move_children "source", "target" move_contents "source", "target" inreplace "foo.conf", "@PREFIX@", "{{HOMEBREW_PREFIX}}" symlink "source", "target", source_base: :relative, overwrite: true, remove_on_uninstall: true @@ -120,6 +118,84 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do CASK end + it "autocorrects legacy install step names" do + expect_offense <<~CASK + cask "foo" do + version :latest + sha256 :no_check + + preflight_steps do + mkdir "foo" + ^^^^^ Use `mkdir_p` instead of legacy install step `mkdir`. + mv "source", "target" + ^^ Use `move` instead of legacy install step `mv`. + move_children "source", "target" + ^^^^^^^^^^^^^ Use `move_contents` instead of legacy install step `move_children`. + ln_s "source", "target" + ^^^^ Use `symlink` instead of legacy install step `ln_s`. + ln_sf "source", "target" + ^^^^^ Use `symlink` instead of legacy install step `ln_sf`. + write "foo.conf", "content" + ^^^^^ Use `write_file` instead of legacy install step `write`. + write "banner", <<~TEXT + ^^^^^ Use `write_file` instead of legacy install step `write`. + banner + TEXT + delete_keychain_certificate "Charles", matching_certificate: "certificate.pem" + ^^^^^^^^^^^^^^^^^^^^ Use `fingerprint_of:` instead of legacy install step keyword `matching_certificate:`. + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `delete_keychain_certificates` instead of legacy install step `delete_keychain_certificate`. + end + end + CASK + + expect_correction <<~CASK + cask "foo" do + version :latest + sha256 :no_check + + preflight_steps do + mkdir_p "foo" + move "source", "target" + move_contents "source", "target" + symlink "source", "target" + symlink "source", "target", overwrite: true + write_file "foo.conf", "content", overwrite: false, append_newline: true + write_file "banner", <<~TEXT, overwrite: false, append_newline: true + banner + TEXT + delete_keychain_certificates "Charles", fingerprint_of: "certificate.pem" + end + end + CASK + end + + it "autocorrects legacy install step keywords" do + expect_offense <<~CASK + cask "foo" do + version :latest + sha256 :no_check + + preflight_steps do + delete_keychain_certificates "Charles", + matching_certificate: "certificate.pem" + ^^^^^^^^^^^^^^^^^^^^ Use `fingerprint_of:` instead of legacy install step keyword `matching_certificate:`. + end + end + CASK + + expect_correction <<~CASK + cask "foo" do + version :latest + sha256 :no_check + + preflight_steps do + delete_keychain_certificates "Charles", + fingerprint_of: "certificate.pem" + end + end + CASK + end + it "reports an offense when a step string uses unsupported interpolation" do expect_offense <<~'CASK' cask "foo" do diff --git a/Library/Homebrew/test/rubocops/install_steps_spec.rb b/Library/Homebrew/test/rubocops/install_steps_spec.rb index c9dc254b0e..ed0f884223 100644 --- a/Library/Homebrew/test/rubocops/install_steps_spec.rb +++ b/Library/Homebrew/test/rubocops/install_steps_spec.rb @@ -13,12 +13,13 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do ) end - it "allows `post_install` and `post_install_steps` in third-party taps during incremental conversion" do - expect_no_offenses(<<~RUBY, "/Taps/example/homebrew-core/Formula/f/foo.rb") + it "rejects `post_install` and `post_install_steps` in third-party taps" do + expect_offense(<<~RUBY, "/Taps/example/homebrew-core/Formula/f/foo.rb") class Foo < Formula url "https://brew.sh/foo-1.0.tgz" post_install_steps do + ^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: `post_install` and `post_install_steps` cannot both be used. touch "foo/state", base: :var end @@ -27,12 +28,13 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do RUBY end - it "rejects `post_install` in official Homebrew taps" do + it "rejects `post_install` and `post_install_steps` in official Homebrew taps" do expect_offense(<<~RUBY, "/Taps/homebrew/homebrew-example/Formula/f/foo.rb") class Foo < Formula url "https://brew.sh/foo-1.0.tgz" post_install_steps do + ^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: `post_install` and `post_install_steps` cannot both be used. touch "foo/state", base: :var end @@ -42,14 +44,15 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do RUBY end - it "leaves `post_install_steps` ordering to `ComponentsOrder`" do - expect_no_offenses(<<~RUBY) + it "rejects coexistence regardless of component order" do + expect_offense(<<~RUBY) class Foo < Formula url "https://brew.sh/foo-1.0.tgz" def post_install; end post_install_steps do + ^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: `post_install` and `post_install_steps` cannot both be used. touch "foo/state", base: :var end end @@ -64,8 +67,8 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do post_install_steps do mkdir_p "log/foo" ^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Formula install-step paths must specify their base explicitly. - write "foo/state", "ready" - ^^^^^^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Formula install-step paths must specify their base explicitly. + write_file "foo/state", "ready" + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Formula install-step paths must specify their base explicitly. init_data_dir "foo", using: :postgresql_initdb ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Formula install-step paths must specify their base explicitly. if_path_exists "foo/state" do @@ -85,7 +88,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do post_install_steps do mkdir_p "log/foo", base: :var - write "foo/state", "ready", base: :var + write_file "foo/state", "ready", base: :var init_data_dir "foo", using: :postgresql_initdb, base: :var if_path_exists "foo/state", base: :var do touch "foo/checked", base: :var @@ -128,7 +131,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do mkdir_p "log/foo", base: :var touch "{{var}}/foo/state" if_path_exists "/etc/foo.conf" do - write "foo.conf", "ready", base: :etc + write_file "foo.conf", "ready", base: :etc end run "foo", base: :bin, chdir: "{{libexec}}/foo" end @@ -143,7 +146,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. Prefer canonical calls: `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children`, `write_file`, `init_data_dir`, `compile_gsettings_schemas`, `update_gdk_pixbuf_loaders_cache`, `update_gtk_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`, `bootstrap_cpython`, `bootstrap_pypy`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`. + ^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls. Prefer canonical calls: `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children`, `write_file`, `init_data_dir`, `compile_gsettings_schemas`, `update_gio_modules_cache`, `update_gdk_pixbuf_loaders_cache`, `update_gtk_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`, `bootstrap_cpython`, `bootstrap_pypy`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`. end end RUBY @@ -199,8 +202,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do symlink_tree "source", "#{formula_name}" symlink_children "source", suffix: "-#{version.major}" compile_gsettings_schemas - gio_querymodules - gdk_pixbuf_query_loaders + update_gio_modules_cache update_gdk_pixbuf_loaders_cache update_gtk_icon_cache update_mime_database @@ -220,6 +222,118 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do RUBY end + it "autocorrects legacy install step names" do + expect_offense(<<~RUBY) + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + + post_install_steps do + mkdir "foo", base: :var + ^^^^^ FormulaAudit/InstallSteps: Use `mkdir_p` instead of legacy install step `mkdir`. + mv "source", "target", + ^^ FormulaAudit/InstallSteps: Use `move` instead of legacy install step `mv`. + force: true + ^^^^^ FormulaAudit/InstallSteps: Use `overwrite:` instead of legacy install step keyword `force:`. + move_children "source", "target" + ^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Use `move_contents` instead of legacy install step `move_children`. + ln_s "source", "target" + ^^^^ FormulaAudit/InstallSteps: Use `symlink` instead of legacy install step `ln_s`. + ln_sf "source", "target", + ^^^^^ FormulaAudit/InstallSteps: Use `symlink` instead of legacy install step `ln_sf`. + uninstall: true + ^^^^^^^^^ FormulaAudit/InstallSteps: Use `remove_on_uninstall:` instead of legacy install step keyword `uninstall:`. + link_dir "source", "target" + ^^^^^^^^ FormulaAudit/InstallSteps: Use `symlink_tree` instead of legacy install step `link_dir`. + link_children "source", "target" + ^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Use `symlink_children` instead of legacy install step `link_children`. + write "foo.conf", "content", base: :var + ^^^^^ FormulaAudit/InstallSteps: Use `write_file` instead of legacy install step `write`. + gio_querymodules + ^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Use `update_gio_modules_cache` instead of legacy install step `gio_querymodules`. + gdk_pixbuf_query_loaders + ^^^^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Use `update_gdk_pixbuf_loaders_cache` instead of legacy install step `gdk_pixbuf_query_loaders`. + gtk_update_icon_cache + ^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Use `update_gtk_icon_cache` instead of legacy install step `gtk_update_icon_cache`. + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + + post_install_steps do + mkdir_p "foo", base: :var + move "source", "target", + overwrite: true + move_contents "source", "target" + symlink "source", "target" + symlink "source", "target", + remove_on_uninstall: true, overwrite: true + symlink_tree "source", "target" + symlink_children "source", "target" + write_file "foo.conf", "content", base: :var, overwrite: false, append_newline: true + update_gio_modules_cache + update_gdk_pixbuf_loaders_cache + update_gtk_icon_cache + end + end + RUBY + end + + it "autocorrects legacy install step keywords" do + expect_offense(<<~RUBY) + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + + post_install_steps do + move "source", "target", + force: true + ^^^^^ FormulaAudit/InstallSteps: Use `overwrite:` instead of legacy install step keyword `force:`. + symlink "source", "target", + force: true, + ^^^^^ FormulaAudit/InstallSteps: Use `overwrite:` instead of legacy install step keyword `force:`. + uninstall: true + ^^^^^^^^^ FormulaAudit/InstallSteps: Use `remove_on_uninstall:` instead of legacy install step keyword `uninstall:`. + move "redundant", "false", + force: false + ^^^^^ FormulaAudit/InstallSteps: Use `overwrite:` instead of legacy install step keyword `force:`. + move "combined", "options", + force: true, + ^^^^^ FormulaAudit/InstallSteps: Use `overwrite:` instead of legacy install step keyword `force:`. + overwrite: false + symlink "combined", "options", + force: true, + ^^^^^ FormulaAudit/InstallSteps: Use `overwrite:` instead of legacy install step keyword `force:`. + overwrite: false, + uninstall: true, + ^^^^^^^^^ FormulaAudit/InstallSteps: Use `remove_on_uninstall:` instead of legacy install step keyword `uninstall:`. + remove_on_uninstall: false + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + + post_install_steps do + move "source", "target", + overwrite: true + symlink "source", "target", + overwrite: true, + remove_on_uninstall: true + move "redundant", "false" + move "combined", "options", + overwrite: true + symlink "combined", "options", + overwrite: true, + remove_on_uninstall: true + end + end + RUBY + end + it "reports an offense when a scope contains Ruby code" do expect_offense(<<~RUBY) class Foo < Formula @@ -228,7 +342,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. Prefer canonical calls: `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children`, `write_file`, `init_data_dir`, `compile_gsettings_schemas`, `update_gdk_pixbuf_loaders_cache`, `update_gtk_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`, `bootstrap_cpython`, `bootstrap_pypy`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`. + ^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls. Prefer canonical calls: `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children`, `write_file`, `init_data_dir`, `compile_gsettings_schemas`, `update_gio_modules_cache`, `update_gdk_pixbuf_loaders_cache`, `update_gtk_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`, `bootstrap_cpython`, `bootstrap_pypy`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`. end end end @@ -242,7 +356,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do post_install_steps do write_file "foo.conf", "prefix = #{prefix}", base: :var - ^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls. Prefer canonical calls: `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children`, `write_file`, `init_data_dir`, `compile_gsettings_schemas`, `update_gdk_pixbuf_loaders_cache`, `update_gtk_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`, `bootstrap_cpython`, `bootstrap_pypy`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`. + ^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls. Prefer canonical calls: `mkdir_p`, `touch`, `move`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `symlink_tree`, `symlink_children`, `write_file`, `init_data_dir`, `compile_gsettings_schemas`, `update_gio_modules_cache`, `update_gdk_pixbuf_loaders_cache`, `update_gtk_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`, `bootstrap_cpython`, `bootstrap_pypy`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`. end end RUBY @@ -366,6 +480,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do url "https://brew.sh/foo-1.0.tgz" post_install_steps do + ^^^^^^^^^^^^^^^^^^^^^ FormulaAudit/InstallSteps: `post_install` and `post_install_steps` cannot both be used. touch "postgresql/state", base: :var end diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index 054b99fb8c..13f626fdc5 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -646,7 +646,7 @@ Relative paths default to `staged_path` for `base:`, `source_base:` and `target_ * `remove`: remove one or more paths; example: `remove ["Shared/old", "Shared/*.bak"], recursive: true`. * `inreplace`: replace a string or regular expression in a file; example: `inreplace "Shared/foo.conf", "@PREFIX@", "{{HOMEBREW_PREFIX}}"`. * `symlink`: create a symlink; example: `symlink "Shared/payload", "Payload", source_base: :relative`. -* `write_file`: atomically write exact literal content, replacing an existing file; example: `write_file "Shared/foo.conf", "key = value\n"`. +* `write_file`: atomically write literal content, replacing an existing file by default; pass `append_newline: true` to ensure a trailing newline or `overwrite: false` to preserve it; example: `write_file "Shared/foo.conf", "key = value\n"`. * `delete_keychain_certificates`: delete macOS keychain certificates whose common name matches the argument; example: `delete_keychain_certificates "Charles"`. Pass `fingerprint_of:` with a local certificate path to delete only the matching SHA-256 fingerprint; example: `delete_keychain_certificates "NodeMITMProxyCA", fingerprint_of: "~/Library/Application Support/betwixt/ssl/certs/ca.pem"`. * `set_permissions`: recursively change existing path permissions with `chmod`; example: `set_permissions "Shared/payload", "0755"`. diff --git a/docs/Formula-Cookbook.md b/docs/Formula-Cookbook.md index 782efde688..28cb6ce625 100644 --- a/docs/Formula-Cookbook.md +++ b/docs/Formula-Cookbook.md @@ -1067,7 +1067,7 @@ end Formulae in official Homebrew taps must represent post-install work with [`post_install_steps`](/rubydoc/Formula.html#post_install_steps-class_method); new `post_install` methods are rejected. These steps can be re-run separately with `brew postinstall `, are stored in the JSON API and do not require downloading source formula Ruby. A `post_install_steps` block may only contain the supported step calls with literal arguments. It cannot call the wider formula DSL or arbitrary Ruby code. Homebrew executes the steps with the same post-install sandbox policy. -The legacy `post_install` method remains available temporarily for third-party tap compatibility, but is not an authoring interface for official formulae. +The legacy `post_install` method remains available temporarily for third-party tap compatibility, but is not an authoring interface for official formulae. A formula cannot define both `post_install` and `post_install_steps`. ```ruby class Foo < Formula @@ -1114,7 +1114,7 @@ end #### Default config and template steps -`write_file` atomically writes its exact literal content, replacing an existing file. Specify its `base:`, such as `base: :etc`. Use `unless_path_exists` when a default file should preserve user edits across upgrades: +`write_file` atomically writes its literal content, replacing an existing file by default. Specify its `base:`, such as `base: :etc`. Pass `append_newline: true` to ensure the content ends in a newline, `overwrite: false` to preserve an existing file or use `unless_path_exists` to guard a group of default-file steps: ```ruby unless_path_exists "foo.conf", base: :etc do @@ -1195,6 +1195,7 @@ symlink_children "bin", suffix: "-{{version.major}}" These steps rebuild shared desktop and cache state using Homebrew-owned tools. * `compile_gsettings_schemas`: compile GSettings schemas in `share/glib-2.0/schemas`. +* `update_gio_modules_cache`: update the GIO module cache in `lib/gio/modules`. * `update_gdk_pixbuf_loaders_cache`: update the GDK Pixbuf loader cache. * `update_gtk_icon_cache`: refresh the `hicolor` GTK icon cache. * `update_mime_database`: rebuild the shared MIME database in `share/mime`.