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:
commit
25e4476317
|
|
@ -150,26 +150,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
/// prompt whenever brew needs admin rights (no terminal is attached to
|
||||
/// Process). If two brew commands overlap, the later one's text wins —
|
||||
/// 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]
|
||||
.appendingPathComponent("BrewBar", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
|
||||
// the text lands inside shell single quotes AND an AppleScript string;
|
||||
// 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 script = """
|
||||
#!/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? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path)
|
||||
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 {
|
||||
let brew = self.resolveBrewPath()
|
||||
let cleaned = command.replacingOccurrences(of: "brew ", with: "")
|
||||
|
|
@ -184,7 +186,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
process.arguments = arguments
|
||||
|
||||
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
|
||||
|
||||
let pipe = Pipe()
|
||||
|
|
@ -223,16 +225,40 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
|
||||
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
|
||||
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
|
||||
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 {
|
||||
|
|
@ -319,13 +345,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
func runAutoUpgrade(formulae: Bool, casks: Bool) {
|
||||
statusMenuItem.title = "Auto-upgrading..."
|
||||
|
||||
let caskNames = casks ? cachedOutdated.casks.map(\.name) : []
|
||||
let upgradeCasksThenRefresh = {
|
||||
if casks {
|
||||
let command = Settings.includeGreedyCasks ? "upgrade --cask --greedy" : "upgrade --cask"
|
||||
self.runBrew(command) { _ in
|
||||
self.fetchOutdated(allowAutoUpgrade: false)
|
||||
}
|
||||
} else {
|
||||
self.upgradeCasksSequentially(caskNames) {
|
||||
self.fetchOutdated(allowAutoUpgrade: false)
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +392,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
@objc func upgradeSingle(_ sender: NSMenuItem) {
|
||||
guard let formula = sender.representedObject as? String else { return }
|
||||
|
||||
runBrew("upgrade \(formula)") { _ in
|
||||
runBrew("upgrade \(formula)", askpassMessage: "upgrade \(formula)") { _ in
|
||||
self.refreshAll()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue