Skip to content

Commit 5d630cc

Browse files
authored
Merge pull request #21097 from ahmedadan/feat/add-flatpak-to-brewfile
feat: add flatpak support to brewfile
2 parents 585ff58 + 5bb6610 commit 5d630cc

35 files changed

+1167
-109
lines changed

Library/Homebrew/bundle.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ def go_installed?
6767
@go_installed ||= which_go.present?
6868
end
6969

70+
sig { returns(T.nilable(Pathname)) }
71+
def which_flatpak
72+
@which_flatpak ||= which("flatpak", ORIGINAL_PATHS)
73+
end
74+
75+
sig { returns(T::Boolean) }
76+
def flatpak_installed?
77+
@flatpak_installed ||= which_flatpak.present?
78+
end
79+
7080
sig { returns(T::Boolean) }
7181
def cask_installed?
7282
@cask_installed ||= File.directory?("#{HOMEBREW_PREFIX}/Caskroom") &&
@@ -145,6 +155,8 @@ def reset!
145155
@which_vscode = T.let(nil, T.nilable(Pathname))
146156
@which_go = T.let(nil, T.nilable(Pathname))
147157
@go_installed = T.let(nil, T.nilable(T::Boolean))
158+
@which_flatpak = T.let(nil, T.nilable(Pathname))
159+
@flatpak_installed = T.let(nil, T.nilable(T::Boolean))
148160
@cask_installed = T.let(nil, T.nilable(T::Boolean))
149161
@formula_versions_from_env = T.let(nil, T.nilable(T::Hash[String, String]))
150162
@upgrade_formulae = T.let(nil, T.nilable(T::Array[String]))

Library/Homebrew/bundle/checker.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def find_actionable(entries, exit_on_first_error: false, no_upgrade: false, verb
6666
formulae_to_install: "Formulae",
6767
formulae_to_start: "Services",
6868
go_packages_to_install: "Go Packages",
69+
flatpaks_to_install: "Flatpaks",
6970
}.freeze
7071

7172
def self.check(global: false, file: nil, exit_on_first_error: false, no_upgrade: false, verbose: false)
@@ -146,6 +147,14 @@ def self.go_packages_to_install(exit_on_first_error: false, no_upgrade: false, v
146147
)
147148
end
148149

150+
def self.flatpaks_to_install(exit_on_first_error: false, no_upgrade: false, verbose: false)
151+
require "bundle/flatpak_checker"
152+
Homebrew::Bundle::Checker::FlatpakChecker.new.find_actionable(
153+
@dsl.entries,
154+
exit_on_first_error:, no_upgrade:, verbose:,
155+
)
156+
end
157+
149158
def self.reset!
150159
require "bundle/cask_dumper"
151160
require "bundle/formula_dumper"

Library/Homebrew/bundle/commands/cleanup.rb

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def self.reset!
1313
require "bundle/formula_dumper"
1414
require "bundle/tap_dumper"
1515
require "bundle/vscode_extension_dumper"
16+
require "bundle/flatpak_dumper"
1617
require "bundle/brew_services"
1718

1819
@dsl = nil
@@ -22,17 +23,19 @@ def self.reset!
2223
Homebrew::Bundle::FormulaDumper.reset!
2324
Homebrew::Bundle::TapDumper.reset!
2425
Homebrew::Bundle::VscodeExtensionDumper.reset!
26+
Homebrew::Bundle::FlatpakDumper.reset!
2527
Homebrew::Bundle::BrewServices.reset!
2628
end
2729

2830
def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
29-
formulae: true, casks: true, taps: true, vscode: true)
31+
formulae: true, casks: true, taps: true, vscode: true, flatpak: true)
3032
@dsl ||= dsl
3133

3234
casks = casks ? casks_to_uninstall(global:, file:) : []
3335
formulae = formulae ? formulae_to_uninstall(global:, file:) : []
3436
taps = taps ? taps_to_untap(global:, file:) : []
3537
vscode_extensions = vscode ? vscode_extensions_to_uninstall(global:, file:) : []
38+
flatpaks = flatpak ? flatpaks_to_uninstall(global:, file:) : []
3639
if force
3740
if casks.any?
3841
args = zap ? ["--zap"] : []
@@ -53,6 +56,13 @@ def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
5356
end
5457
end
5558

59+
if flatpaks.any?
60+
flatpaks.each do |flatpak_name|
61+
Kernel.system "flatpak", "uninstall", "-y", "--system", flatpak_name
62+
end
63+
puts "Uninstalled #{flatpaks.size} flatpak#{"s" if flatpaks.size != 1}"
64+
end
65+
5666
cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup")
5767
puts cleanup unless cleanup.empty?
5868
else
@@ -82,6 +92,12 @@ def self.run(global: false, file: nil, force: false, zap: false, dsl: nil,
8292
would_uninstall = true
8393
end
8494

95+
if flatpaks.any?
96+
puts "Would uninstall flatpaks:"
97+
puts Formatter.columns flatpaks
98+
would_uninstall = true
99+
end
100+
85101
cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup", "--dry-run")
86102
unless cleanup.empty?
87103
puts "Would `brew cleanup`:"
@@ -212,6 +228,23 @@ def self.vscode_extensions_to_uninstall(global: false, file: nil)
212228
current_extensions - kept_extensions
213229
end
214230

231+
def self.flatpaks_to_uninstall(global: false, file: nil)
232+
return [].freeze unless Bundle.flatpak_installed?
233+
234+
require "bundle/brewfile"
235+
@dsl ||= Brewfile.read(global:, file:)
236+
kept_flatpaks = @dsl.entries.select { |e| e.type == :flatpak }.map(&:name)
237+
238+
# To provide a graceful migration from `Brewfile`s that don't yet or
239+
# don't want to use `flatpak`: don't remove any flatpaks if we don't
240+
# find any in the `Brewfile`.
241+
return [].freeze if kept_flatpaks.empty?
242+
243+
require "bundle/flatpak_dumper"
244+
current_flatpaks = Homebrew::Bundle::FlatpakDumper.packages
245+
current_flatpaks - kept_flatpaks
246+
end
247+
215248
def self.system_output_no_stderr(cmd, *args)
216249
IO.popen([cmd, *args], err: :close).read
217250
end

Library/Homebrew/bundle/commands/dump.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ module Dump
1010
sig {
1111
params(global: T::Boolean, file: T.nilable(String), describe: T::Boolean, force: T::Boolean,
1212
no_restart: T::Boolean, taps: T::Boolean, formulae: T::Boolean, casks: T::Boolean,
13-
mas: T::Boolean, vscode: T::Boolean, go: T::Boolean).void
13+
mas: T::Boolean, vscode: T::Boolean, go: T::Boolean, flatpak: T::Boolean).void
1414
}
1515
def self.run(global:, file:, describe:, force:, no_restart:, taps:, formulae:, casks:, mas:,
16-
vscode:, go:)
16+
vscode:, go:, flatpak:)
1717
Homebrew::Bundle::Dumper.dump_brewfile(
1818
global:, file:, describe:, force:, no_restart:, taps:, formulae:, casks:, mas:, vscode:,
19-
go:
19+
go:, flatpak:
2020
)
2121
end
2222
end

Library/Homebrew/bundle/commands/list.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ module List
1111
sig {
1212
params(global: T::Boolean, file: T.nilable(String), formulae: T::Boolean, casks: T::Boolean,
1313
taps: T::Boolean, mas: T::Boolean, vscode: T::Boolean,
14-
go: T::Boolean).void
14+
go: T::Boolean, flatpak: T::Boolean).void
1515
}
16-
def self.run(global:, file:, formulae:, casks:, taps:, mas:, vscode:, go:)
16+
def self.run(global:, file:, formulae:, casks:, taps:, mas:, vscode:, go:, flatpak:)
1717
parsed_entries = Brewfile.read(global:, file:).entries
1818
Homebrew::Bundle::Lister.list(
1919
parsed_entries,
20-
formulae:, casks:, taps:, mas:, vscode:, go:,
20+
formulae:, casks:, taps:, mas:, vscode:, go:, flatpak:,
2121
)
2222
end
2323
end

Library/Homebrew/bundle/dsl.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ def go(name)
8282
@entries << Entry.new(:go, name)
8383
end
8484

85+
sig { params(name: String, options: T::Hash[Symbol, String]).void }
86+
def flatpak(name, options = {})
87+
# Validate: url: can only be used with a named remote (not a URL remote)
88+
if options[:url] && options[:remote]&.start_with?("http://", "https://")
89+
raise "url: parameter cannot be used when remote: is already a URL"
90+
end
91+
92+
# Default remote to "flathub"
93+
options[:remote] ||= "flathub"
94+
95+
@entries << Entry.new(:flatpak, name, options)
96+
end
97+
8598
def tap(name, clone_target = nil, options = {})
8699
raise "name(#{name.inspect}) should be a String object" unless name.is_a? String
87100
if clone_target && !clone_target.is_a?(String)

Library/Homebrew/bundle/dumper.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ module Dumper
2424
mas: T::Boolean,
2525
vscode: T::Boolean,
2626
go: T::Boolean,
27+
flatpak: T::Boolean,
2728
).returns(String)
2829
}
29-
def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:, vscode:, go:)
30+
def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:, vscode:, go:, flatpak:)
3031
require "bundle/tap_dumper"
3132
require "bundle/formula_dumper"
3233
require "bundle/cask_dumper"
3334
require "bundle/mac_app_store_dumper"
3435
require "bundle/vscode_extension_dumper"
3536
require "bundle/go_dumper"
37+
require "bundle/flatpak_dumper"
3638

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

@@ -57,13 +60,14 @@ def self.build_brewfile(describe:, no_restart:, formulae:, taps:, casks:, mas:,
5760
mas: T::Boolean,
5861
vscode: T::Boolean,
5962
go: T::Boolean,
63+
flatpak: T::Boolean,
6064
).void
6165
}
6266
def self.dump_brewfile(global:, file:, describe:, force:, no_restart:, formulae:, taps:, casks:, mas:,
63-
vscode:, go:)
67+
vscode:, go:, flatpak:)
6468
path = brewfile_path(global:, file:)
6569
can_write_to_brewfile?(path, force:)
66-
content = build_brewfile(describe:, no_restart:, taps:, formulae:, casks:, mas:, vscode:, go:)
70+
content = build_brewfile(describe:, no_restart:, taps:, formulae:, casks:, mas:, vscode:, go:, flatpak:)
6771
write_file path, content
6872
end
6973

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "bundle/checker"
5+
6+
module Homebrew
7+
module Bundle
8+
module Checker
9+
class FlatpakChecker < Homebrew::Bundle::Checker::Base
10+
PACKAGE_TYPE = :flatpak
11+
PACKAGE_TYPE_NAME = "Flatpak"
12+
13+
sig {
14+
params(entries: T::Array[Homebrew::Bundle::Dsl::Entry], exit_on_first_error: T::Boolean,
15+
no_upgrade: T::Boolean, verbose: T::Boolean).returns(T::Array[String])
16+
}
17+
def find_actionable(entries, exit_on_first_error: false, no_upgrade: false, verbose: false)
18+
super
19+
end
20+
21+
# Override to return entry hashes with options instead of just names
22+
sig { params(entries: T::Array[Bundle::Dsl::Entry]).returns(T::Array[T::Hash[Symbol, T.untyped]]) }
23+
def format_checkable(entries)
24+
checkable_entries(entries).map do |entry|
25+
{ name: entry.name, options: entry.options || {} }
26+
end
27+
end
28+
29+
sig { params(package: T.any(String, T::Hash[Symbol, T.untyped]), no_upgrade: T::Boolean).returns(String) }
30+
def failure_reason(package, no_upgrade:)
31+
name = package.is_a?(Hash) ? package[:name] : package
32+
"#{PACKAGE_TYPE_NAME} #{name} needs to be installed."
33+
end
34+
35+
sig {
36+
params(package: T.any(String, T::Hash[Symbol, T.untyped]), no_upgrade: T::Boolean).returns(T::Boolean)
37+
}
38+
def installed_and_up_to_date?(package, no_upgrade: false)
39+
require "bundle/flatpak_installer"
40+
41+
if package.is_a?(Hash)
42+
name = package[:name]
43+
remote = package.dig(:options, :remote) || "flathub"
44+
url = package.dig(:options, :url)
45+
46+
# 3-tier remote handling:
47+
# - Tier 1: Named remote → check with that remote name
48+
# - Tier 2: URL only → resolve to single-app remote name (<app-id>-origin)
49+
# - Tier 3: URL + name → check with the named remote
50+
actual_remote = if url.blank? && remote.start_with?("http://", "https://")
51+
# Tier 2: URL only - resolve to single-app remote name
52+
# (.flatpakref - check by name only since remote name varies)
53+
return Homebrew::Bundle::FlatpakInstaller.package_installed?(name) if remote.end_with?(".flatpakref")
54+
55+
Homebrew::Bundle::FlatpakInstaller.generate_single_app_remote_name(name)
56+
else
57+
# Tier 1 (named remote) and Tier 3 (named remote with URL) both use the remote name
58+
remote
59+
end
60+
61+
Homebrew::Bundle::FlatpakInstaller.package_installed?(name, remote: actual_remote)
62+
else
63+
# If just a string, check without remote
64+
Homebrew::Bundle::FlatpakInstaller.package_installed?(package)
65+
end
66+
end
67+
end
68+
end
69+
end
70+
end

0 commit comments

Comments
 (0)