2023-04-01 18:56:42 -07:00
|
|
|
# typed: true
|
2020-07-27 10:37:46 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-11-05 17:17:03 -05:00
|
|
|
# Helper functions for updating PyPI resources.
|
2020-08-26 09:39:51 +02:00
|
|
|
#
|
|
|
|
# @api private
|
2020-07-27 10:37:46 -04:00
|
|
|
module PyPI
|
2020-11-22 15:23:43 -05:00
|
|
|
extend T::Sig
|
|
|
|
|
2020-07-27 10:37:46 -04:00
|
|
|
PYTHONHOSTED_URL_PREFIX = "https://files.pythonhosted.org/packages/"
|
2020-08-26 09:39:51 +02:00
|
|
|
private_constant :PYTHONHOSTED_URL_PREFIX
|
2020-07-27 10:37:46 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
# PyPI Package
|
|
|
|
#
|
|
|
|
# @api private
|
|
|
|
class Package
|
|
|
|
extend T::Sig
|
2020-07-31 10:10:07 -04:00
|
|
|
|
2023-03-13 23:59:25 +00:00
|
|
|
attr_accessor :name, :extras, :version
|
2020-07-31 10:10:07 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
sig { params(package_string: String, is_url: T::Boolean).void }
|
|
|
|
def initialize(package_string, is_url: false)
|
|
|
|
@pypi_info = nil
|
2020-07-31 10:10:07 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
if is_url
|
2021-01-07 13:49:05 -08:00
|
|
|
match = if package_string.start_with?(PYTHONHOSTED_URL_PREFIX)
|
|
|
|
File.basename(package_string).match(/^(.+)-([a-z\d.]+?)(?:.tar.gz|.zip)$/)
|
2020-11-22 15:23:43 -05:00
|
|
|
end
|
2023-02-10 23:15:40 -05:00
|
|
|
raise ArgumentError, "Package should be a valid PyPI URL" if match.blank?
|
2020-07-31 10:10:07 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
@name = match[1]
|
|
|
|
@version = match[2]
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2023-04-01 18:56:42 -07:00
|
|
|
if package_string.include? "=="
|
|
|
|
@name, @version = package_string.split("==")
|
|
|
|
else
|
|
|
|
@name = package_string
|
|
|
|
end
|
2020-11-22 15:23:43 -05:00
|
|
|
|
2023-04-01 18:56:42 -07:00
|
|
|
return unless (match = T.must(@name).match(/^(.*?)\[(.+)\]$/))
|
2020-11-22 15:23:43 -05:00
|
|
|
|
|
|
|
@name = match[1]
|
2023-04-01 18:56:42 -07:00
|
|
|
@extras = T.must(match[2]).split ","
|
2020-11-22 15:23:43 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# Get name, URL, SHA-256 checksum, and latest version for a given PyPI package.
|
|
|
|
sig { params(version: T.nilable(T.any(String, Version))).returns(T.nilable(T::Array[String])) }
|
|
|
|
def pypi_info(version: nil)
|
|
|
|
return @pypi_info if @pypi_info.present? && version.blank?
|
|
|
|
|
|
|
|
version ||= @version
|
|
|
|
metadata_url = if version.present?
|
|
|
|
"https://pypi.org/pypi/#{@name}/#{version}/json"
|
|
|
|
else
|
|
|
|
"https://pypi.org/pypi/#{@name}/json"
|
|
|
|
end
|
2022-06-10 18:47:33 +01:00
|
|
|
out, _, status = curl_output metadata_url, "--location", "--fail"
|
2020-11-22 15:23:43 -05:00
|
|
|
|
|
|
|
return unless status.success?
|
|
|
|
|
|
|
|
begin
|
|
|
|
json = JSON.parse out
|
|
|
|
rescue JSON::ParserError
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
sdist = json["urls"].find { |url| url["packagetype"] == "sdist" }
|
|
|
|
return json["info"]["name"] if sdist.nil?
|
|
|
|
|
|
|
|
@pypi_info = [json["info"]["name"], sdist["url"], sdist["digests"]["sha256"], json["info"]["version"]]
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(T::Boolean) }
|
|
|
|
def valid_pypi_package?
|
|
|
|
info = pypi_info
|
|
|
|
info.present? && info.is_a?(Array)
|
|
|
|
end
|
|
|
|
|
|
|
|
sig { returns(String) }
|
|
|
|
def to_s
|
|
|
|
out = @name
|
|
|
|
out += "[#{@extras.join(",")}]" if @extras.present?
|
|
|
|
out += "==#{@version}" if @version.present?
|
|
|
|
out
|
2020-11-18 02:25:55 -05:00
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
sig { params(other: Package).returns(T::Boolean) }
|
|
|
|
def same_package?(other)
|
2023-04-01 18:56:42 -07:00
|
|
|
T.must(@name.tr("_", "-").casecmp(other.name.tr("_", "-"))).zero?
|
2020-11-22 15:23:43 -05:00
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
|
2021-11-14 03:26:40 -08:00
|
|
|
# Compare only names so we can use .include? and .uniq on a Package array
|
2020-11-22 15:23:43 -05:00
|
|
|
sig { params(other: Package).returns(T::Boolean) }
|
|
|
|
def ==(other)
|
|
|
|
same_package?(other)
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|
2021-11-14 03:26:40 -08:00
|
|
|
alias eql? ==
|
|
|
|
|
|
|
|
sig { returns(Integer) }
|
|
|
|
def hash
|
|
|
|
@name.tr("_", "-").downcase.hash
|
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
sig { params(other: Package).returns(T.nilable(Integer)) }
|
|
|
|
def <=>(other)
|
|
|
|
@name <=> other.name
|
|
|
|
end
|
|
|
|
end
|
2020-07-31 14:21:44 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
sig { params(url: String, version: T.any(String, Version)).returns(T.nilable(String)) }
|
2023-04-01 18:56:42 -07:00
|
|
|
def self.update_pypi_url(url, version)
|
2020-11-22 15:23:43 -05:00
|
|
|
package = Package.new url, is_url: true
|
|
|
|
|
2020-12-03 15:00:30 -05:00
|
|
|
return unless package.valid_pypi_package?
|
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
_, url = package.pypi_info(version: version)
|
|
|
|
url
|
2020-12-03 15:00:30 -05:00
|
|
|
rescue ArgumentError
|
|
|
|
nil
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|
|
|
|
|
2020-11-05 17:17:03 -05:00
|
|
|
# Return true if resources were checked (even if no change).
|
2021-01-17 22:45:55 -08:00
|
|
|
sig {
|
2020-11-22 15:23:43 -05:00
|
|
|
params(
|
|
|
|
formula: Formula,
|
|
|
|
version: T.nilable(String),
|
|
|
|
package_name: T.nilable(String),
|
|
|
|
extra_packages: T.nilable(T::Array[String]),
|
|
|
|
exclude_packages: T.nilable(T::Array[String]),
|
2020-11-29 22:36:40 +01:00
|
|
|
print_only: T.nilable(T::Boolean),
|
|
|
|
silent: T.nilable(T::Boolean),
|
|
|
|
ignore_non_pypi_packages: T.nilable(T::Boolean),
|
2020-11-22 15:23:43 -05:00
|
|
|
).returns(T.nilable(T::Boolean))
|
2021-01-17 22:45:55 -08:00
|
|
|
}
|
2023-04-01 18:56:42 -07:00
|
|
|
def self.update_python_resources!(formula, version: nil, package_name: nil, extra_packages: nil,
|
|
|
|
exclude_packages: nil, print_only: false, silent: false,
|
|
|
|
ignore_non_pypi_packages: false)
|
2020-11-18 02:25:55 -05:00
|
|
|
|
2020-11-24 16:52:29 -05:00
|
|
|
auto_update_list = formula.tap&.pypi_formula_mappings
|
2020-11-20 01:55:34 -05:00
|
|
|
if auto_update_list.present? && auto_update_list.key?(formula.full_name) &&
|
|
|
|
package_name.blank? && extra_packages.blank? && exclude_packages.blank?
|
2020-11-18 02:25:55 -05:00
|
|
|
|
|
|
|
list_entry = auto_update_list[formula.full_name]
|
|
|
|
case list_entry
|
|
|
|
when false
|
2020-11-20 01:55:34 -05:00
|
|
|
unless print_only
|
|
|
|
odie "The resources for \"#{formula.name}\" need special attention. Please update them manually."
|
|
|
|
end
|
2020-11-18 02:25:55 -05:00
|
|
|
when String
|
|
|
|
package_name = list_entry
|
|
|
|
when Hash
|
|
|
|
package_name = list_entry["package_name"]
|
|
|
|
extra_packages = list_entry["extra_packages"]
|
|
|
|
exclude_packages = list_entry["exclude_packages"]
|
|
|
|
end
|
2020-07-31 16:42:53 -04:00
|
|
|
end
|
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
main_package = if package_name.present?
|
|
|
|
Package.new(package_name)
|
|
|
|
else
|
|
|
|
begin
|
|
|
|
Package.new(formula.stable.url, is_url: true)
|
|
|
|
rescue ArgumentError
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
2020-11-18 02:25:55 -05:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
if main_package.blank?
|
2020-09-15 12:12:47 -07:00
|
|
|
return if ignore_non_pypi_packages
|
|
|
|
|
|
|
|
odie <<~EOS
|
|
|
|
Could not infer PyPI package name from URL:
|
|
|
|
#{Formatter.url(formula.stable.url)}
|
|
|
|
EOS
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
unless main_package.valid_pypi_package?
|
|
|
|
return if ignore_non_pypi_packages
|
2020-11-18 02:25:55 -05:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
odie "\"#{main_package}\" is not available on PyPI."
|
2020-11-18 02:25:55 -05:00
|
|
|
end
|
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
main_package.version = version if version.present?
|
2020-11-18 02:25:55 -05:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
extra_packages = (extra_packages || []).map { |p| Package.new p }
|
|
|
|
exclude_packages = (exclude_packages || []).map { |p| Package.new p }
|
2021-03-14 13:40:34 -04:00
|
|
|
exclude_packages += %W[#{main_package.name} argparse pip setuptools wsgiref].map { |p| Package.new p }
|
2021-01-18 09:01:15 -05:00
|
|
|
# remove packages from the exclude list if we've explicitly requested them as an extra package
|
|
|
|
exclude_packages.delete_if { |package| extra_packages.include?(package) }
|
2020-11-22 15:23:43 -05:00
|
|
|
|
|
|
|
input_packages = [main_package]
|
|
|
|
extra_packages.each do |extra_package|
|
|
|
|
if !extra_package.valid_pypi_package? && !ignore_non_pypi_packages
|
|
|
|
odie "\"#{extra_package}\" is not available on PyPI."
|
|
|
|
end
|
|
|
|
|
|
|
|
input_packages.each do |existing_package|
|
|
|
|
if existing_package.same_package?(extra_package) && existing_package.version != extra_package.version
|
2022-06-28 10:09:59 +01:00
|
|
|
odie "Conflicting versions specified for the `#{extra_package.name}` package: " \
|
2021-07-06 23:44:09 +05:30
|
|
|
"#{existing_package.version}, #{extra_package.version}"
|
2020-11-22 15:23:43 -05:00
|
|
|
end
|
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
input_packages << extra_package unless input_packages.include? extra_package
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
formula.resources.each do |resource|
|
|
|
|
if !print_only && !resource.url.start_with?(PYTHONHOSTED_URL_PREFIX)
|
|
|
|
odie "\"#{formula.name}\" contains non-PyPI resources. Please update the resources manually."
|
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|
|
|
|
|
2022-01-11 17:12:04 +00:00
|
|
|
ensure_formula_installed!("pipgrip")
|
2020-07-31 16:56:21 +01:00
|
|
|
|
2021-01-14 17:01:43 +00:00
|
|
|
ohai "Retrieving PyPI dependencies for \"#{input_packages.join(" ")}\"..." if !print_only && !silent
|
2021-11-14 03:26:40 -08:00
|
|
|
command =
|
|
|
|
[Formula["pipgrip"].opt_bin/"pipgrip", "--json", "--tree", "--no-cache-dir", *input_packages.map(&:to_s)]
|
2021-01-14 17:01:43 +00:00
|
|
|
pipgrip_output = Utils.popen_read(*command)
|
|
|
|
unless $CHILD_STATUS.success?
|
|
|
|
odie <<~EOS
|
2022-12-13 10:54:22 +00:00
|
|
|
Unable to determine dependencies for "#{input_packages.join(" ")}" because of a failure when running
|
2021-01-14 17:01:43 +00:00
|
|
|
`#{command.join(" ")}`.
|
2022-12-13 10:54:22 +00:00
|
|
|
Please update the resources for "#{formula.name}" manually.
|
2021-01-14 17:01:43 +00:00
|
|
|
EOS
|
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
|
2021-11-14 03:26:40 -08:00
|
|
|
found_packages = json_to_packages(JSON.parse(pipgrip_output), main_package, exclude_packages).uniq
|
2020-07-27 10:37:46 -04:00
|
|
|
|
|
|
|
new_resource_blocks = ""
|
2020-11-22 15:23:43 -05:00
|
|
|
found_packages.sort.each do |package|
|
2020-11-18 02:25:55 -05:00
|
|
|
if exclude_packages.include? package
|
2020-11-22 15:23:43 -05:00
|
|
|
ohai "Excluding \"#{package}\"" if !print_only && !silent
|
2020-11-18 02:25:55 -05:00
|
|
|
next
|
|
|
|
end
|
|
|
|
|
2020-11-22 15:23:43 -05:00
|
|
|
ohai "Getting PyPI info for \"#{package}\"" if !print_only && !silent
|
|
|
|
name, url, checksum = package.pypi_info
|
2020-07-27 10:37:46 -04:00
|
|
|
# Fail if unable to find name, url or checksum for any resource
|
|
|
|
if name.blank?
|
|
|
|
odie "Unable to resolve some dependencies. Please update the resources for \"#{formula.name}\" manually."
|
|
|
|
elsif url.blank? || checksum.blank?
|
|
|
|
odie <<~EOS
|
2022-12-13 10:54:22 +00:00
|
|
|
Unable to find the URL and/or sha256 for the "#{name}" resource.
|
|
|
|
Please update the resources for "#{formula.name}" manually.
|
2020-07-27 10:37:46 -04:00
|
|
|
EOS
|
|
|
|
end
|
|
|
|
|
|
|
|
# Append indented resource block
|
|
|
|
new_resource_blocks += <<-EOS
|
|
|
|
resource "#{name}" do
|
|
|
|
url "#{url}"
|
|
|
|
sha256 "#{checksum}"
|
|
|
|
end
|
|
|
|
|
|
|
|
EOS
|
|
|
|
end
|
|
|
|
|
|
|
|
if print_only
|
|
|
|
puts new_resource_blocks.chomp
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2020-10-06 23:22:07 -04:00
|
|
|
# Check whether resources already exist (excluding virtualenv dependencies)
|
|
|
|
if formula.resources.all? { |resource| resource.name.start_with?("homebrew-") }
|
2020-07-27 10:37:46 -04:00
|
|
|
# Place resources above install method
|
|
|
|
inreplace_regex = / def install/
|
|
|
|
new_resource_blocks += " def install"
|
|
|
|
else
|
|
|
|
# Replace existing resource blocks with new resource blocks
|
|
|
|
inreplace_regex = / (resource .* do\s+url .*\s+sha256 .*\s+ end\s*)+/
|
|
|
|
new_resource_blocks += " "
|
|
|
|
end
|
|
|
|
|
|
|
|
ohai "Updating resource blocks" unless silent
|
|
|
|
Utils::Inreplace.inreplace formula.path do |s|
|
|
|
|
if s.inreplace_string.scan(inreplace_regex).length > 1
|
|
|
|
odie "Unable to update resource blocks for \"#{formula.name}\" automatically. Please update them manually."
|
|
|
|
end
|
|
|
|
s.sub! inreplace_regex, new_resource_blocks
|
|
|
|
end
|
2020-09-17 21:35:01 -04:00
|
|
|
|
|
|
|
true
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|
2021-11-14 03:26:40 -08:00
|
|
|
|
2023-04-01 18:56:42 -07:00
|
|
|
def self.json_to_packages(json_tree, main_package, exclude_packages)
|
2021-11-15 10:49:07 -08:00
|
|
|
return [] if json_tree.blank?
|
2021-11-14 03:26:40 -08:00
|
|
|
|
|
|
|
json_tree.flat_map do |package_json|
|
|
|
|
package = Package.new("#{package_json["name"]}==#{package_json["version"]}")
|
2021-11-15 10:49:07 -08:00
|
|
|
dependencies = if package == main_package || exclude_packages.exclude?(package)
|
2021-11-14 03:26:40 -08:00
|
|
|
json_to_packages(package_json["dependencies"], main_package, exclude_packages)
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
end
|
2021-11-15 10:49:07 -08:00
|
|
|
[package] + dependencies
|
2021-11-14 03:26:40 -08:00
|
|
|
end
|
|
|
|
end
|
2020-07-27 10:37:46 -04:00
|
|
|
end
|