Skip to content

cask: gracefully handle removed DSL methods#21624

Merged
MikeMcQuaid merged 5 commits intoHomebrew:mainfrom
costajohnt:fix/appcast-uninstall-error
Feb 28, 2026
Merged

cask: gracefully handle removed DSL methods#21624
MikeMcQuaid merged 5 commits intoHomebrew:mainfrom
costajohnt:fix/appcast-uninstall-error

Conversation

@costajohnt
Copy link
Contributor

@costajohnt costajohnt commented Feb 24, 2026

Summary

Casks installed before the appcast stanza was removed cannot be managed (brew uninstall, brew upgrade, etc.) because loading the cached .rb caskfile triggers DSL#method_missing, which calls ofail and sets Homebrew.failed = true without raising an exception.

Approach

Per review feedback, this moves error handling to the loader level following the formulary.rb rescue pattern:

  1. DSL#method_missing now raises NoMethodError instead of calling ofail. This makes unknown/removed DSL methods a hard error instead of silently poisoning global state.

  2. FromPathLoader#load catches CaskInvalidError and wraps it as CaskUnreadableError when @from_installed_caskfile is set. This flag is only set by FromInstalledPathLoader (used for stale cached .rb files). Normal cask loading re-raises CaskInvalidError unchanged.

  3. Error propagation chain: NoMethodErrorCask#refresh catches it → raises CaskInvalidErrorFromPathLoader#load wraps as CaskUnreadableError (installed only) → named_args.rb fallback (line 410) catches it and creates a minimal cask → uninstall/upgrade succeeds.

This follows the same pattern as formulary.rb's error handling — let invalid definitions fail to load, selectively soften errors for installed caskfiles, and handle the failure at the caller level based on the operation.

Before: brew uninstall --cask qlimagesizeError: Unexpected method 'appcast'... + Homebrew.failed = true (exit code 1, but cask still loads with corrupted state)

After: brew uninstall --cask qlimagesizeCaskInvalidError → wrapped as CaskUnreadableError for installed caskfile → named_args.rb fallback creates minimal cask → uninstall succeeds

Fixes #21602

Test Plan

  • Added FromPathLoader test verifying cask with removed DSL method raises CaskInvalidError
  • Added FromPathLoader test verifying installed caskfiles with removed DSL methods raise CaskUnreadableError
  • Added test verifying Homebrew.failed is NOT set after the error
  • Updated existing unknown-method DSL test to expect CaskInvalidError
  • brew tests --only cask/dsl,cask/cask_loader passes (447 examples, 0 failures)

@costajohnt costajohnt force-pushed the fix/appcast-uninstall-error branch from 008a6c0 to 8e30488 Compare February 24, 2026 22:34
@costajohnt costajohnt marked this pull request as ready for review February 24, 2026 22:59
Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @costajohnt! This is good progress but I'm not sure the right approach yet.

ofail sets a failing error code and just prints an Error: message so it's not really doing much different to opoo's Warning: equivalent.

Instead, this should involve changing some logic in cask_loader.rb (see rescue NameError, ArgumentError, ScriptError, MethodDeprecatedError, MacOSVersion::Error in formulary.rb for similar logic) so that we don't try to read invalid casks at all. For certain operations (e.g. no to info, yes to uninstall) we will also need an override in this case so that we can still uninstall even with no readable cask .rb or .json file.

Happy to continue to give feedback or answer questions, thanks!

@costajohnt costajohnt force-pushed the fix/appcast-uninstall-error branch 2 times, most recently from bf610ee to d888d5c Compare February 25, 2026 23:20
Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@costajohnt Looking much better, thanks! Can you confirm you've manually tested the info, upgrade and uninstall cases and found them all to work as expected now?

Casks installed before `appcast` was removed from the DSL cannot be
managed (uninstall/upgrade/info) because loading the cached .rb
caskfile triggers `method_missing`, which calls `ofail` and sets
`Homebrew.failed = true`.

Add a `REMOVED_METHODS` set to the DSL class. When a removed method
is encountered, issue a warning (`opoo`) instead of an error (`ofail`)
so the cask still loads cleanly and operations like `uninstall` exit
with code 0.

Fixes Homebrew#21602
Move error handling from DSL method_missing (opoo/ofail) to
the loader level. Unknown DSL methods now raise NoMethodError,
which Cask#refresh converts to CaskInvalidError. This lets the
existing fallback infrastructure in named_args.rb handle it
gracefully for uninstall/upgrade operations.
Remove unreachable `if method` guard in DSL#method_missing
(Ruby's method_missing always receives a Symbol, which is
always truthy). Combine test assertions so the Homebrew.failed
check also verifies the exception is raised.
…files

When loading an installed caskfile that contains removed DSL methods,
wrap the resulting CaskInvalidError as CaskUnreadableError so the
existing fallback in named_args.rb can handle it gracefully. This is
gated on @from_installed_caskfile to avoid affecting normal cask
loading, following the same pattern as formulary.rb.
- respond_to_missing? now returns false to match method_missing raising
  NoMethodError (previously returned true unconditionally)
- Test CaskUnreadableError message includes the removed method name
- Add comment explaining rescue ordering dependency between cask.rb and
  cask_loader.rb
@costajohnt costajohnt force-pushed the fix/appcast-uninstall-error branch from d888d5c to 9ab570c Compare February 27, 2026 20:07
@costajohnt
Copy link
Contributor Author

@MikeMcQuaid Manually tested all three cases against a simulated installed cask with the removed appcast stanza in its cached .rb file:

  • brew info --cask qlimagesize - Cask 'qlimagesize' is unreadable: undefined method 'appcast' for Cask 'qlimagesize'. Expected since the cask was removed from the tap, so there's no API data to fall back on.
  • brew upgrade --cask qlimagesize - Same unreadable error. Expected because the cask is no longer in the tap.
  • brew uninstall --cask qlimagesize - Successfully uninstalls: Purging files for version 2.6.1 of Cask qlimagesize.

All three handle the removed DSL method gracefully. No NoMethodError crash and no Homebrew.failed side effect.
Screenshot 2026-02-27 at 12 06 28 PM

Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks again @costajohnt!

@MikeMcQuaid MikeMcQuaid added this pull request to the merge queue Feb 28, 2026
Merged via the queue into Homebrew:main with commit 255dc83 Feb 28, 2026
36 checks passed
@costajohnt costajohnt deleted the fix/appcast-uninstall-error branch February 28, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot uninstall/upgrade casks installed with removed 'appcast' method

2 participants