Merge branch 'per-cask-upgrades': sudo prompts name the app being upgraded

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-07 05:56:40 +02:00
commit 25e4476317

View file

@ -150,26 +150,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
/// prompt whenever brew needs admin rights (no terminal is attached to /// prompt whenever brew needs admin rights (no terminal is attached to
/// Process). If two brew commands overlap, the later one's text wins /// Process). If two brew commands overlap, the later one's text wins
/// harmless, since prompts realistically only appear during upgrades. /// harmless, since prompts realistically only appear during upgrades.
static func writeAskpassScript(for command: String) -> URL { static func writeAskpassScript(message: String) -> URL {
let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("BrewBar", isDirectory: true) .appendingPathComponent("BrewBar", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
// the text lands inside shell single quotes AND an AppleScript string; // the text lands inside shell single quotes AND an AppleScript string;
// whitelist characters that cannot break out of either // whitelist characters that cannot break out of either
let safeCommand = command.filter { $0.isLetter || $0.isNumber || " ._@+=:/-".contains($0) } let safeMessage = message.filter { $0.isLetter || $0.isNumber || " ._@+=:/-".contains($0) }
let url = dir.appendingPathComponent("askpass.sh") let url = dir.appendingPathComponent("askpass.sh")
let script = """ let script = """
#!/bin/zsh #!/bin/zsh
osascript -e 'display dialog "BrewBar needs your administrator password to run:\\n\\nbrew \(safeCommand)" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result' osascript -e 'display dialog "BrewBar needs your administrator password to \(safeMessage)." default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result'
""" """
try? script.write(to: url, atomically: true, encoding: .utf8) try? script.write(to: url, atomically: true, encoding: .utf8)
try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path) try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path)
return url return url
} }
func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) { /// askpassMessage customizes the sudo dialog ("...password to <message>.");
/// defaults to naming the brew command.
func runBrew(_ command: String, askpassMessage: String? = nil, completion: @escaping (String) -> Void = { _ in }) {
DispatchQueue.global().async { DispatchQueue.global().async {
let brew = self.resolveBrewPath() let brew = self.resolveBrewPath()
let cleaned = command.replacingOccurrences(of: "brew ", with: "") let cleaned = command.replacingOccurrences(of: "brew ", with: "")
@ -184,7 +186,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
process.arguments = arguments process.arguments = arguments
var environment = ProcessInfo.processInfo.environment var environment = ProcessInfo.processInfo.environment
environment["SUDO_ASKPASS"] = Self.writeAskpassScript(for: cleaned).path environment["SUDO_ASKPASS"] = Self.writeAskpassScript(message: askpassMessage ?? "run: brew \(cleaned)").path
process.environment = environment process.environment = environment
let pipe = Pipe() let pipe = Pipe()
@ -223,16 +225,40 @@ class AppDelegate: NSObject, NSApplicationDelegate {
statusMenuItem.title = "Upgrading..." statusMenuItem.title = "Upgrading..."
// must match the outdated listing: greedy-listed casks are skipped by
// plain "upgrade" and would stay outdated forever
let command = Settings.includeGreedyCasks ? "upgrade --greedy" : "upgrade"
runBrew("update") { _ in runBrew("update") { _ in
self.runBrew(command) { _ in // packages that turn outdated only after this brew update are not
// in cachedOutdated yet; the final refetch will surface them
let caskNames = self.cachedOutdated.casks.map(\.name)
let upgradeCasksThenRefresh = {
self.upgradeCasksSequentially(caskNames) {
// everything was just upgraded; don't chain an auto-upgrade // everything was just upgraded; don't chain an auto-upgrade
self.fetchOutdated(allowAutoUpgrade: false) self.fetchOutdated(allowAutoUpgrade: false)
} }
} }
if self.cachedOutdated.formulae.isEmpty {
upgradeCasksThenRefresh()
} else {
self.runBrew("upgrade --formula") { _ in
upgradeCasksThenRefresh()
}
}
}
}
/// Upgrades casks one at a time so each sudo prompt can name the app it
/// is for a batched "upgrade --cask" gives no per-package hook.
func upgradeCasksSequentially(_ names: [String], completion: @escaping () -> Void) {
guard let next = names.first else {
completion()
return
}
statusMenuItem.title = "Upgrading \(next)..."
// an explicitly named cask upgrades even when self-updating, no --greedy needed
runBrew("upgrade --cask \(next)", askpassMessage: "upgrade \(next)") { _ in
self.upgradeCasksSequentially(Array(names.dropFirst()), completion: completion)
}
} }
func confirmUpgrade() -> Bool { func confirmUpgrade() -> Bool {
@ -319,13 +345,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func runAutoUpgrade(formulae: Bool, casks: Bool) { func runAutoUpgrade(formulae: Bool, casks: Bool) {
statusMenuItem.title = "Auto-upgrading..." statusMenuItem.title = "Auto-upgrading..."
let caskNames = casks ? cachedOutdated.casks.map(\.name) : []
let upgradeCasksThenRefresh = { let upgradeCasksThenRefresh = {
if casks { self.upgradeCasksSequentially(caskNames) {
let command = Settings.includeGreedyCasks ? "upgrade --cask --greedy" : "upgrade --cask"
self.runBrew(command) { _ in
self.fetchOutdated(allowAutoUpgrade: false)
}
} else {
self.fetchOutdated(allowAutoUpgrade: false) self.fetchOutdated(allowAutoUpgrade: false)
} }
} }
@ -370,7 +392,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc func upgradeSingle(_ sender: NSMenuItem) { @objc func upgradeSingle(_ sender: NSMenuItem) {
guard let formula = sender.representedObject as? String else { return } guard let formula = sender.representedObject as? String else { return }
runBrew("upgrade \(formula)") { _ in runBrew("upgrade \(formula)", askpassMessage: "upgrade \(formula)") { _ in
self.refreshAll() self.refreshAll()
} }
} }