Skip to content

Commit d3bee9e

Browse files
authored
Merge pull request #22301 from Homebrew/embed-executables-api-json
Embed executables in API JSON
2 parents 2d00563 + 2a13176 commit d3bee9e

32 files changed

Lines changed: 347 additions & 237 deletions

Library/Homebrew/api.rb

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,13 @@ def self.fetch_api_files!
214214
end
215215
end
216216

217-
sig { void }
218-
def self.write_names_and_aliases
217+
sig { params(legacy_executables_fallback: T::Boolean).void }
218+
def self.write_names_and_aliases(legacy_executables_fallback: false)
219219
if Homebrew::EnvConfig.use_internal_api?
220-
Homebrew::API::Internal.write_formula_names_and_aliases
220+
Homebrew::API::Internal.write_formula_names_and_aliases(legacy_executables_fallback:)
221221
Homebrew::API::Internal.write_cask_names
222222
else
223-
Homebrew::API::Formula.write_names_and_aliases
223+
Homebrew::API::Formula.write_names_and_aliases(legacy_executables_fallback:)
224224
Homebrew::API::Cask.write_names
225225
end
226226
end
@@ -252,6 +252,80 @@ def self.write_aliases_file!(aliases, type, regenerate:)
252252
false
253253
end
254254

255+
sig {
256+
params(
257+
formulae: T::Hash[String, T::Hash[String, T.untyped]],
258+
regenerate: T::Boolean,
259+
legacy_executables_fallback: T::Boolean,
260+
).returns(T::Boolean)
261+
}
262+
def self.write_executables_file!(formulae, regenerate:, legacy_executables_fallback: false)
263+
executables_path = HOMEBREW_CACHE_API/"internal/executables.txt"
264+
executables_lines = formulae.filter_map do |name, hash|
265+
executables = T.cast(hash["executables"], T.nilable(T::Array[String]))
266+
next if executables.blank?
267+
268+
"#{name}:#{executables.join(" ")}"
269+
end
270+
if executables_lines.empty?
271+
return false if executables_path.exist? && !executables_path.empty?
272+
return false unless legacy_executables_fallback
273+
274+
# TODO: Remove this fallback once formula JSON always includes executables.
275+
return download_executables_file_from_github_packages!(executables_path)
276+
end
277+
278+
contents = "#{executables_lines.sort.join("\n")}\n"
279+
if !executables_path.exist? || regenerate || executables_path.read != contents
280+
executables_path.dirname.mkpath
281+
executables_path.unlink if executables_path.exist?
282+
executables_path.write(contents)
283+
return true
284+
end
285+
286+
false
287+
end
288+
289+
sig { params(target: Pathname).returns(T::Boolean) }
290+
def self.download_executables_file_from_github_packages!(target)
291+
github_packages_url = "https://ghcr.io/v2/homebrew/command-not-found/executables"
292+
manifest_args = [
293+
"--fail", "--location",
294+
"--header", "Accept: application/vnd.oci.image.manifest.v1+json",
295+
"#{github_packages_url}/manifests/latest"
296+
]
297+
if HOMEBREW_GITHUB_PACKAGES_AUTH.present?
298+
manifest_args.insert(-2, "--header", "Authorization: #{HOMEBREW_GITHUB_PACKAGES_AUTH}")
299+
end
300+
301+
manifest_output = Utils::Curl.curl_output(*manifest_args, show_error: false)
302+
return false unless manifest_output.success?
303+
304+
manifest = JSON.parse(manifest_output.stdout)
305+
layers = T.cast(manifest.fetch("layers"), T::Array[T::Hash[String, T.untyped]])
306+
layer = layers.find do |candidate|
307+
candidate.dig("annotations", "org.opencontainers.image.title") == target.basename.to_s
308+
end
309+
return false if layer.nil?
310+
311+
digest = T.cast(layer["digest"], T.nilable(String))
312+
return false if digest.blank?
313+
314+
download_args = ["--fail"]
315+
if HOMEBREW_GITHUB_PACKAGES_AUTH.present?
316+
download_args += ["--header", "Authorization: #{HOMEBREW_GITHUB_PACKAGES_AUTH}"]
317+
end
318+
download_args << "#{github_packages_url}/blobs/#{digest}"
319+
target.dirname.mkpath
320+
Utils::Curl.curl_download(*download_args, to: target, show_error: false)
321+
FileUtils.touch(target)
322+
true
323+
rescue ErrorDuringExecution, JSON::ParserError, KeyError, TypeError
324+
target.unlink if target.exist? && target.empty?
325+
326+
false
327+
end
328+
255329
sig {
256330
params(json_data: T::Hash[String, T.untyped])
257331
.returns([T::Boolean, T.any(String, T::Array[T.untyped], T::Hash[String, T.untyped])])

Library/Homebrew/api/formula.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,13 @@ def self.tap_migrations
201201
cache.fetch("tap_migrations")
202202
end
203203

204-
sig { params(regenerate: T::Boolean).void }
205-
def self.write_names_and_aliases(regenerate: false)
204+
sig { params(regenerate: T::Boolean, legacy_executables_fallback: T::Boolean).void }
205+
def self.write_names_and_aliases(regenerate: false, legacy_executables_fallback: false)
206206
download_and_cache_data! unless cache.key?("formulae")
207207

208208
Homebrew::API.write_names_file!(all_formulae.keys, "formula", regenerate:)
209209
Homebrew::API.write_aliases_file!(all_aliases, "formula", regenerate:)
210+
Homebrew::API.write_executables_file!(all_formulae, regenerate:, legacy_executables_fallback:)
210211
end
211212
end
212213
end

Library/Homebrew/api/formula_struct.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def self.from_hash(formula_hash)
9393
const :deprecate_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
9494
const :desc, String
9595
const :disable_args, T::Hash[Symbol, T.nilable(T.any(String, Symbol))], default: {}
96+
const :executables, T::Array[String], default: []
9697
const :head_dependencies, T::Array[DependsOnArgs], default: []
9798
const :head_url_args, [String, T::Hash[Symbol, T.anything]], default: ["", {}]
9899
const :head_uses_from_macos, T::Array[UsesFromMacOSArgs], default: []

Library/Homebrew/api/internal.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,13 @@ def self.download_and_cache_data!
9090
end
9191
private_class_method :download_and_cache_data!
9292

93-
sig { params(regenerate: T::Boolean).void }
94-
def self.write_formula_names_and_aliases(regenerate: false)
93+
sig { params(regenerate: T::Boolean, legacy_executables_fallback: T::Boolean).void }
94+
def self.write_formula_names_and_aliases(regenerate: false, legacy_executables_fallback: false)
9595
download_and_cache_data! unless cache.key?("formula_hashes")
9696

9797
Homebrew::API.write_names_file!(formula_hashes.keys, "formula", regenerate:)
9898
Homebrew::API.write_aliases_file!(formula_aliases, "formula", regenerate:)
99+
Homebrew::API.write_executables_file!(formula_hashes, regenerate:, legacy_executables_fallback:)
99100
end
100101

101102
sig { params(regenerate: T::Boolean).void }

Library/Homebrew/cmd/exec.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Exec < AbstractCommand
1111

1212
cmd_args do
1313
usage_banner <<~EOS
14-
`exec`, `x` [`--skip-update`] [`--formulae=`<formulae>] [`--`] <command> [<args> ...]
14+
`exec`, `x` [`--formulae=`<formulae>] [`--`] <command> [<args> ...]
1515
1616
Run <command> in an environment populated by Homebrew formulae.
1717
@@ -29,8 +29,6 @@ class Exec < AbstractCommand
2929
`#!/usr/bin/env -S brew exec --formulae=jq,yq --`
3030
EOS
3131

32-
switch "--skip-update",
33-
description: "Skip updating the executables database if any version exists on disk, no matter how old."
3432
comma_array "--formulae",
3533
description: "Comma-separated formulae to install and add to `PATH` before running " \
3634
"<command>."

Library/Homebrew/cmd/exec.sh

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,6 @@ homebrew-exec() {
8484
while [[ "$#" -gt 0 ]]
8585
do
8686
case "$1" in
87-
--skip-update)
88-
HOMEBREW_SKIP_UPDATE=1
89-
shift
90-
;;
9187
--formulae=*)
9288
formulae_arg="${1#--formulae=}"
9389
formulae_seen=1
@@ -148,7 +144,7 @@ homebrew-exec() {
148144
[[ "${executable}" != */* ]] || odie "Executable name must not contain path separators without \`--formulae\`."
149145

150146
provider_lookup=1
151-
download_and_cache_executables_file >&2
147+
ensure_executables_file >&2
152148

153149
local -a matching_formulae=()
154150
local selected_formula formula

Library/Homebrew/cmd/update-report.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ def output_update_report
101101
end
102102

103103
# Check if we can parse the JSON and do any Ruby-side follow-up.
104-
Homebrew::API.write_names_and_aliases unless Homebrew::EnvConfig.no_install_from_api?
104+
unless Homebrew::EnvConfig.no_install_from_api?
105+
Homebrew::API.write_names_and_aliases(legacy_executables_fallback: true)
106+
end
105107

106108
Homebrew.failed = true if ENV["HOMEBREW_UPDATE_FAILED"]
107109
return if Homebrew::EnvConfig.disable_load_formula?

Library/Homebrew/cmd/update.sh

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# HOMEBREW_FORCE_BREWED_CURL, HOMEBREW_FORCE_BREWED_GIT,
1212
# HOMEBREW_SYSTEM_CURL_TOO_OLD, HOMEBREW_USER_AGENT_CURL are set by brew.sh
1313
# shellcheck disable=SC2154
14-
source "${HOMEBREW_LIBRARY}/Homebrew/utils/executables.sh"
14+
source "${HOMEBREW_LIBRARY}/Homebrew/utils/api.sh"
1515
source "${HOMEBREW_LIBRARY}/Homebrew/utils/lock.sh"
1616

1717
macos_version_name() {
@@ -428,8 +428,6 @@ fetch_api_file() {
428428
do
429429
time_cond+=("${arg}")
430430
done < <(api_time_cond_args "${cache_path}")
431-
# Keep curl request handling in sync with `download_and_cache_executables_file`
432-
# in Library/Homebrew/utils/executables.sh.
433431
curl \
434432
"${CURL_DISABLE_CURLRC_ARGS[@]}" \
435433
--fail --compressed --silent \
@@ -685,12 +683,6 @@ EOS
685683

686684
safe_cd "${HOMEBREW_REPOSITORY}"
687685

688-
# This means the user has run `brew which-formula` before and we should fetch executables.txt
689-
if [[ "$(git config get --type=bool homebrew.commandnotfound 2>/dev/null)" == "true" ]]
690-
then
691-
export HOMEBREW_FETCH_EXECUTABLES_TXT=1
692-
fi
693-
694686
# if an older system had a newer curl installed, change each repo's remote URL from git to https
695687
if [[ -n "${HOMEBREW_SYSTEM_CURL_TOO_OLD}" && -x "${HOMEBREW_PREFIX}/opt/curl/bin/curl" ]] &&
696688
[[ "$(git config remote.origin.url)" =~ ^git:// ]]
@@ -1032,13 +1024,6 @@ EOS
10321024
fi
10331025
fi
10341026

1035-
# Update executables.txt if the user has ever run `brew which-formula` before.
1036-
if [[ -n "${HOMEBREW_FETCH_EXECUTABLES_TXT}" ]]
1037-
then
1038-
ohai "Downloading executables.txt"
1039-
fetch_api_file "${HOMEBREW_EXECUTABLES_TXT_ENDPOINT}" "${update_failed_file}"
1040-
fi
1041-
10421027
if [[ -f "${update_failed_file}" ]]
10431028
then
10441029
onoe <"${update_failed_file}"

Library/Homebrew/cmd/which-formula.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ class WhichFormula < AbstractCommand
2222
EOS
2323
switch "--explain",
2424
description: "Output explanation of how to get <command> by installing one of the providing formulae."
25-
switch "--skip-update",
26-
description: "Skip updating the executables database if any version exists on disk, no matter how old."
2725
named_args :command, min: 1
2826
end
2927
end

Library/Homebrew/cmd/which-formula.sh

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ homebrew-which-formula() {
1313
HOMEBREW_EXPLAIN=1
1414
shift
1515
;;
16-
--skip-update)
17-
HOMEBREW_SKIP_UPDATE=1
18-
shift
19-
;;
2016
--*)
2117
echo "Unknown option: $1" >&2
2218
brew help which-formula
@@ -37,7 +33,7 @@ homebrew-which-formula() {
3733

3834
for cmd in "${args[@]}"
3935
do
40-
download_and_cache_executables_file
36+
ensure_executables_file
4137

4238
local formulae=()
4339
local formula

0 commit comments

Comments
 (0)