Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Library/Homebrew/bundle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ def go_installed?
@go_installed ||= which_go.present?
end

sig { returns(T.nilable(Pathname)) }
def which_flatpak
@which_flatpak ||= which("flatpak", ORIGINAL_PATHS)
end

sig { returns(T::Boolean) }
def flatpak_installed?
@flatpak_installed ||= which_flatpak.present?
end

sig { returns(T::Boolean) }
def cask_installed?
@cask_installed ||= File.directory?("#{HOMEBREW_PREFIX}/Caskroom") &&
Expand Down Expand Up @@ -145,6 +155,8 @@ def reset!
@which_vscode = T.let(nil, T.nilable(Pathname))
@which_go = T.let(nil, T.nilable(Pathname))
@go_installed = T.let(nil, T.nilable(T::Boolean))
@which_flatpak = T.let(nil, T.nilable(Pathname))
@flatpak_installed = T.let(nil, T.nilable(T::Boolean))
@cask_installed = T.let(nil, T.nilable(T::Boolean))
@formula_versions_from_env = T.let(nil, T.nilable(T::Hash[String, String]))
@upgrade_formulae = T.let(nil, T.nilable(T::Array[String]))
Expand Down
9 changes: 9 additions & 0 deletions Library/Homebrew/bundle/checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def find_actionable(entries, exit_on_first_error: false, no_upgrade: false, verb
formulae_to_install: "Formulae",
formulae_to_start: "Services",
go_packages_to_install: "Go Packages",
flatpaks_to_install: "Flatpaks",
}.freeze

def self.check(global: false, file: nil, exit_on_first_error: false, no_upgrade: false, verbose: false)
Expand Down Expand Up @@ -146,6 +147,14 @@ def self.go_packages_to_install(exit_on_first_error: false, no_upgrade: false, v
)
end

def self.flatpaks_to_install(exit_on_first_error: false, no_upgrade: false, verbose: false)
require "bundle/flatpak_checker"
Homebrew::Bundle::Checker::FlatpakChecker.new.find_actionable(
@dsl.entries,
exit_on_first_error:, no_upgrade:, verbose:,
)
end

def self.reset!
require "bundle/cask_dumper"
require "bundle/formula_dumper"
Expand Down
35 changes: 34 additions & 1 deletion Library/Homebrew/bundle/commands/cleanup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def self.reset!
require "bundle/formula_dumper"
require "bundle/tap_dumper"
require "bundle/vscode_extension_dumper"
require "bundle/flatpak_dumper"
require "bundle/brew_services"

@dsl = nil
Expand All @@ -22,17 +23,19 @@ def self.reset!
Homebrew::Bundle::FormulaDumper.reset!
Homebrew::Bundle::TapDumper.reset!
Homebrew::Bundle::VscodeExtensionDumper.reset!
Homebrew::Bundle::FlatpakDumper.reset!
Homebrew::Bundle::BrewServices.reset!
end

def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
formulae: true, casks: true, taps: true, vscode: true)
formulae: true, casks: true, taps: true, vscode: true, flatpak: true)
@dsl ||= dsl

casks = casks ? casks_to_uninstall(global:, file:) : []
formulae = formulae ? formulae_to_uninstall(global:, file:) : []
taps = taps ? taps_to_untap(global:, file:) : []
vscode_extensions = vscode ? vscode_extensions_to_uninstall(global:, file:) : []
flatpaks = flatpak ? flatpaks_to_uninstall(global:, file:) : []
if force
if casks.any?
args = zap ? ["--zap"] : []
Expand All @@ -53,6 +56,13 @@ def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
end
end

if flatpaks.any?
flatpaks.each do |flatpak_name|
Kernel.system "flatpak", "uninstall", "-y", "--system", flatpak_name
end
puts "Uninstalled #{flatpaks.size} flatpak#{"s" if flatpaks.size != 1}"
end

cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup")
puts cleanup unless cleanup.empty?
else
Expand Down Expand Up @@ -82,6 +92,12 @@ def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
would_uninstall = true
end

if flatpaks.any?
puts "Would uninstall flatpaks:"
puts Formatter.columns flatpaks
would_uninstall = true
end

cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup", "--dry-run")
unless cleanup.empty?
puts "Would `brew cleanup`:"
Expand Down Expand Up @@ -212,6 +228,23 @@ def self.vscode_extensions_to_uninstall(global: false, file: nil)
current_extensions - kept_extensions
end

def self.flatpaks_to_uninstall(global: false, file: nil)
return [].freeze unless Bundle.flatpak_installed?

require "bundle/brewfile"
@dsl ||= Brewfile.read(global:, file:)
kept_flatpaks = @dsl.entries.select { |e| e.type == :flatpak }.map(&:name)

# To provide a graceful migration from `Brewfile`s that don't yet or
# don't want to use `flatpak`: don't remove any flatpaks if we don't
# find any in the `Brewfile`.
return [].freeze if kept_flatpaks.empty?

require "bundle/flatpak_dumper"
current_flatpaks = Homebrew::Bundle::FlatpakDumper.packages
current_flatpaks - kept_flatpaks
end

def self.system_output_no_stderr(cmd, *args)
IO.popen([cmd, *args], err: :close).read
end
Expand Down
6 changes: 3 additions & 3 deletions Library/Homebrew/bundle/commands/dump.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ module Dump
sig {
params(global: T::Boolean, file: T.nilable(String), describe: T::Boolean, force: T::Boolean,
no_restart: T::Boolean, taps: T::Boolean, formulae: T::Boolean, casks: T::Boolean,
mas: T::Boolean, vscode: T::Boolean, go: T::Boolean).void
mas: T::Boolean, vscode: T::Boolean, go: T::Boolean, flatpak: T::Boolean).void
}
def self.run(global:, file:, describe:, force:, no_restart:, taps:, formulae:, casks:, mas:,
vscode:, go:)
vscode:, go:, flatpak:)
Homebrew::Bundle::Dumper.dump_brewfile(
global:, file:, describe:, force:, no_restart:, taps:, formulae:, casks:, mas:, vscode:,
go:
go:, flatpak:
)
end
end
Expand Down
6 changes: 3 additions & 3 deletions Library/Homebrew/bundle/commands/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ module List
sig {
params(global: T::Boolean, file: T.nilable(String), formulae: T::Boolean, casks: T::Boolean,
taps: T::Boolean, mas: T::Boolean, vscode: T::Boolean,
go: T::Boolean).void
go: T::Boolean, flatpak: T::Boolean).void
}
def self.run(global:, file:, formulae:, casks:, taps:, mas:, vscode:, go:)
def self.run(global:, file:, formulae:, casks:, taps:, mas:, vscode:, go:, flatpak:)
parsed_entries = Brewfile.read(global:, file:).entries
Homebrew::Bundle::Lister.list(
parsed_entries,
formulae:, casks:, taps:, mas:, vscode:, go:,
formulae:, casks:, taps:, mas:, vscode:, go:, flatpak:,
)
end
end
Expand Down
13 changes: 13 additions & 0 deletions Library/Homebrew/bundle/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ def go(name)
@entries << Entry.new(:go, name)
end

sig { params(name: String, options: T::Hash[Symbol, String]).void }
def flatpak(name, options = {})
# Validate: url: can only be used with a named remote (not a URL remote)
if options[:url] && options[:remote]&.start_with?("http://", "https://")
raise "url: parameter cannot be used when remote: is already a URL"
end

# Default remote to "flathub"
options[:remote] ||= "flathub"

@entries << Entry.new(:flatpak, name, options)
end

def tap(name, clone_target = nil, options = {})
raise "name(#{name.inspect}) should be a String object" unless name.is_a? String
if clone_target && !clone_target.is_a?(String)
Expand Down
10 changes: 7 additions & 3 deletions Library/Homebrew/bundle/dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ module Dumper
mas: T::Boolean,
vscode: T::Boolean,
go: T::Boolean,
flatpak: T::Boolean,
).returns(String)
}
def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:, vscode:, go:)
def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:, vscode:, go:, flatpak:)
require "bundle/tap_dumper"
require "bundle/formula_dumper"
require "bundle/cask_dumper"
require "bundle/mac_app_store_dumper"
require "bundle/vscode_extension_dumper"
require "bundle/go_dumper"
require "bundle/flatpak_dumper"

content = []
content << TapDumper.dump if taps
Expand All @@ -41,6 +43,7 @@ def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:,
content << MacAppStoreDumper.dump if mas
content << VscodeExtensionDumper.dump if vscode
content << GoDumper.dump if go
content << FlatpakDumper.dump if flatpak
"#{content.reject(&:empty?).join("\n")}\n"
end

Expand All @@ -57,13 +60,14 @@ def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:,
mas: T::Boolean,
vscode: T::Boolean,
go: T::Boolean,
flatpak: T::Boolean,
).void
}
def self.dump_brewfile(global:, file:, describe:, force:, no_restart:, formulae:, taps:, casks:, mas:,
vscode:, go:)
vscode:, go:, flatpak:)
path = brewfile_path(global:, file:)
can_write_to_brewfile?(path, force:)
content = build_brewfile(describe:, no_restart:, taps:, formulae:, casks:, mas:, vscode:, go:)
content = build_brewfile(describe:, no_restart:, taps:, formulae:, casks:, mas:, vscode:, go:, flatpak:)
write_file path, content
end

Expand Down
70 changes: 70 additions & 0 deletions Library/Homebrew/bundle/flatpak_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# typed: strict
# frozen_string_literal: true

require "bundle/checker"

module Homebrew
module Bundle
module Checker
class FlatpakChecker < Homebrew::Bundle::Checker::Base
PACKAGE_TYPE = :flatpak
PACKAGE_TYPE_NAME = "Flatpak"

sig {
params(entries: T::Array[Homebrew::Bundle::Dsl::Entry], exit_on_first_error: T::Boolean,
no_upgrade: T::Boolean, verbose: T::Boolean).returns(T::Array[String])
}
def find_actionable(entries, exit_on_first_error: false, no_upgrade: false, verbose: false)
super
end

# Override to return entry hashes with options instead of just names
sig { params(entries: T::Array[Bundle::Dsl::Entry]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
def format_checkable(entries)
checkable_entries(entries).map do |entry|
{ name: entry.name, options: entry.options || {} }
end
end

sig { params(package: T.any(String, T::Hash[Symbol, T.untyped]), no_upgrade: T::Boolean).returns(String) }
def failure_reason(package, no_upgrade:)
name = package.is_a?(Hash) ? package[:name] : package
"#{PACKAGE_TYPE_NAME} #{name} needs to be installed."
end

sig {
params(package: T.any(String, T::Hash[Symbol, T.untyped]), no_upgrade: T::Boolean).returns(T::Boolean)
}
def installed_and_up_to_date?(package, no_upgrade: false)
require "bundle/flatpak_installer"

if package.is_a?(Hash)
name = package[:name]
remote = package.dig(:options, :remote) || "flathub"
url = package.dig(:options, :url)

# 3-tier remote handling:
# - Tier 1: Named remote → check with that remote name
# - Tier 2: URL only → resolve to single-app remote name (<app-id>-origin)
# - Tier 3: URL + name → check with the named remote
actual_remote = if url.blank? && remote.start_with?("http://", "https://")
# Tier 2: URL only - resolve to single-app remote name
# (.flatpakref - check by name only since remote name varies)
return Homebrew::Bundle::FlatpakInstaller.package_installed?(name) if remote.end_with?(".flatpakref")

Homebrew::Bundle::FlatpakInstaller.generate_single_app_remote_name(name)
else
# Tier 1 (named remote) and Tier 3 (named remote with URL) both use the remote name
remote
end

Homebrew::Bundle::FlatpakInstaller.package_installed?(name, remote: actual_remote)
else
# If just a string, check without remote
Homebrew::Bundle::FlatpakInstaller.package_installed?(package)
end
end
end
end
end
end
Loading
Loading