Skip to content

Commit

Permalink
Merge pull request #9423 from reitermarkus/bump-unversioned-casks
Browse files Browse the repository at this point in the history
Add `bump-unversioned-casks` command.
  • Loading branch information
reitermarkus committed Dec 7, 2020
2 parents ff28d7c + 7344079 commit e416668
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Library/Homebrew/cask/artifact/pkg.rb
Expand Up @@ -15,7 +15,7 @@ module Artifact
#
# @api private
class Pkg < AbstractArtifact
attr_reader :pkg_relative_path, :path, :stanza_options
attr_reader :path, :stanza_options

def self.from_args(cask, path, **stanza_options)
stanza_options.assert_valid_keys!(:allow_untrusted, :choices)
Expand Down
2 changes: 2 additions & 0 deletions Library/Homebrew/cask/cask.rbi
Expand Up @@ -2,6 +2,8 @@

module Cask
class Cask
def artifacts; end

def homepage; end
end
end
4 changes: 4 additions & 0 deletions Library/Homebrew/cask/download.rb
Expand Up @@ -40,6 +40,10 @@ def downloader
end
end

def time_file_size
downloader.resolved_time_file_size
end

def clear_cache
downloader.clear_cache
end
Expand Down
27 changes: 13 additions & 14 deletions Library/Homebrew/cask/installer.rb
Expand Up @@ -30,7 +30,8 @@ class Installer
def initialize(cask, command: SystemCommand, force: false,
skip_cask_deps: false, binaries: true, verbose: false,
require_sha: false, upgrade: false,
installed_as_dependency: false, quarantine: true)
installed_as_dependency: false, quarantine: true,
verify_download_integrity: true)
@cask = cask
@command = command
@force = force
Expand All @@ -42,6 +43,7 @@ def initialize(cask, command: SystemCommand, force: false,
@upgrade = upgrade
@installed_as_dependency = installed_as_dependency
@quarantine = quarantine
@verify_download_integrity = verify_download_integrity
end

attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?,
Expand Down Expand Up @@ -150,13 +152,10 @@ def summary
s.freeze
end

sig { returns(Pathname) }
def download
return @downloaded_path if @downloaded_path

odebug "Downloading"
@downloaded_path = Download.new(@cask, quarantine: quarantine?).fetch
odebug "Downloaded to -> #{@downloaded_path}"
@downloaded_path
@download ||= Download.new(@cask, quarantine: quarantine?)
.fetch(verify_download_integrity: @verify_download_integrity)
end

def verify_has_sha
Expand All @@ -171,15 +170,15 @@ def verify_has_sha

def primary_container
@primary_container ||= begin
download
UnpackStrategy.detect(@downloaded_path, type: @cask.container&.type, merge_xattrs: true)
downloaded_path = download
UnpackStrategy.detect(downloaded_path, type: @cask.container&.type, merge_xattrs: true)
end
end

def extract_primary_container
def extract_primary_container(to: @cask.staged_path)
odebug "Extracting primary container"

odebug "Using container class #{primary_container.class} for #{@downloaded_path}"
odebug "Using container class #{primary_container.class} for #{primary_container.path}"

basename = CGI.unescape(File.basename(@cask.url.path))

Expand All @@ -191,16 +190,16 @@ def extract_primary_container
FileUtils.chmod_R "+rw", tmpdir/nested_container, force: true, verbose: verbose?

UnpackStrategy.detect(tmpdir/nested_container, merge_xattrs: true)
.extract_nestedly(to: @cask.staged_path, verbose: verbose?)
.extract_nestedly(to: to, verbose: verbose?)
end
else
primary_container.extract_nestedly(to: @cask.staged_path, basename: basename, verbose: verbose?)
primary_container.extract_nestedly(to: to, basename: basename, verbose: verbose?)
end

return unless quarantine?
return unless Quarantine.available?

Quarantine.propagate(from: @downloaded_path, to: @cask.staged_path)
Quarantine.propagate(from: primary_container.path, to: to)
end

def install_artifacts
Expand Down
24 changes: 13 additions & 11 deletions Library/Homebrew/dev-cmd/bump-cask-pr.rb
Expand Up @@ -72,9 +72,10 @@ def bump_cask_pr
new_version = Cask::DSL::Version.new(new_version)
new_base_url = args.url
new_hash = args.sha256
new_hash = :no_check if new_hash == ":no_check"

old_version = cask.version
old_hash = cask.sha256.to_s
old_hash = cask.sha256

tap_full_name = cask.tap&.full_name
default_remote_branch = cask.tap.path.git_origin_branch if cask.tap
Expand All @@ -95,7 +96,7 @@ def bump_cask_pr
elsif old_version.latest?
opoo "No --url= argument specified!" unless new_base_url
elsif new_version.latest?
opoo "Ignoring specified --sha256= argument." if new_hash
opoo "Ignoring specified --sha256= argument." if new_hash && new_check != :no_check
elsif Version.new(new_version) < Version.new(old_version)
odie <<~EOS
You need to bump this cask manually since changing the
Expand Down Expand Up @@ -136,7 +137,9 @@ def bump_cask_pr
]
end

if !new_version.latest? && (new_hash.nil? || cask.languages.present?)
if new_version.latest?
new_hash = :no_check
elsif new_hash.nil? || cask.languages.present?
tmp_contents = Utils::Inreplace.inreplace_pairs(cask.sourcefile_path,
replacement_pairs.uniq.compact,
read_only_run: true,
Expand Down Expand Up @@ -172,15 +175,14 @@ def bump_cask_pr
end
end

replacement_pairs << if old_version.latest?
[
"sha256 :no_check",
"sha256 \"#{new_hash}\"",
]
elsif new_version.latest?
p old_hash

replacement_pairs << if old_version.latest? || new_version.latest? || new_hash == :no_check
hash_regex = old_hash == :no_check ? ":no_check" : "[\"']#{Regexp.escape(old_hash.to_s)}[\"']"

[
"sha256 \"#{old_hash}\"",
"sha256 :no_check",
/sha256\s+#{hash_regex}/m,
"sha256 #{new_hash == :no_check ? ":no_check" : "\"#{new_hash}\""}",
]
else
[
Expand Down
175 changes: 175 additions & 0 deletions Library/Homebrew/dev-cmd/bump-unversioned-casks.rb
@@ -0,0 +1,175 @@
# typed: false
# frozen_string_literal: true

require "cask/download"
require "cask/installer"
require "cask/cask_loader"
require "cli/parser"
require "tap"
require "unversioned_cask_checker"

module Homebrew
extend T::Sig

extend SystemCommand::Mixin

sig { returns(CLI::Parser) }
def self.bump_unversioned_casks_args
Homebrew::CLI::Parser.new do
usage_banner <<~EOS
`bump-unversioned-casks` [<options>] [<tap>]
Check all casks with unversioned URLs in a given <tap> for updates.
EOS
switch "-n", "--dry-run",
description: "Do everything except caching state and opening pull requests."
flag "--limit=",
description: "Maximum runtime in minutes."
flag "--state-file=",
description: "File for caching state."

named 1
end
end

sig { void }
def self.bump_unversioned_casks
args = bump_unversioned_casks_args.parse

state_file = if args.state_file.present?
Pathname(args.state_file).expand_path
else
HOMEBREW_CACHE/"bump_unversioned_casks.json"
end
state_file.dirname.mkpath

tap = Tap.fetch(args.named.first)

state = state_file.exist? ? JSON.parse(state_file.read) : {}

cask_files = tap.cask_files
unversioned_cask_files = cask_files.select do |cask_file|
url = cask_file.each_line do |line|
url = line[/\s*url\s+"([^"]+)"\s*/, 1]
break url if url
end

url.present? && url.exclude?('#{')
end.sort

unversioned_casks = unversioned_cask_files.map { |path| Cask::CaskLoader.load(path) }

ohai "Unversioned Casks: #{unversioned_casks.count}"

checked, unchecked = unversioned_casks.partition { |c| state.key?(c.full_name) }

queue = Queue.new

# Start with random casks which have not been checked.
unchecked.shuffle.each do |c|
queue.enq c
end

# Continue with previously checked casks, ordered by when they were last checked.
checked.sort_by { |c| state.dig(c.full_name, "check_time") }.each do |c|
queue.enq c
end

limit = args.limit.presence&.to_i
end_time = Time.now + limit.minutes if limit

until queue.empty? || (end_time && end_time < Time.now)
cask = queue.deq

key = cask.full_name

new_state = bump_unversioned_cask(cask, state: state.fetch(key, {}), dry_run: args.dry_run?)

next unless new_state

state[key] = new_state

state_file.atomic_write JSON.generate(state) unless args.dry_run?
end
end

sig do
params(cask: Cask::Cask, state: T::Hash[String, T.untyped], dry_run: T.nilable(T::Boolean))
.returns(T.nilable(T::Hash[String, T.untyped]))
end
def self.bump_unversioned_cask(cask, state:, dry_run:)
ohai "Checking #{cask.full_name}"

unversioned_cask_checker = UnversionedCaskChecker.new(cask)

unless unversioned_cask_checker.single_app_cask? || unversioned_cask_checker.single_pkg_cask?
opoo "Skipping, not a single-app or PKG cask."
return
end

last_check_time = state["check_time"]&.yield_self { |t| Time.parse(t) }

check_time = Time.now
if last_check_time && check_time < (last_check_time + 1.day)
opoo "Skipping, already checked within the last 24 hours."
return
end

last_sha256 = state["sha256"]
last_time = state["time"]&.yield_self { |t| Time.parse(t) }
last_file_size = state["file_size"]

download = Cask::Download.new(cask)
time, file_size = begin
download.time_file_size
rescue
[nil, nil]
end

if last_time != time || last_file_size != file_size
begin
cached_download = unversioned_cask_checker.installer.download
rescue => e
onoe e
return
end

sha256 = cached_download.sha256

if last_sha256 != sha256 && (version = unversioned_cask_checker.guess_cask_version)
if cask.version == version
oh1 "Cask #{cask} is up-to-date at #{version}"
else
bump_cask_pr_args = [
"bump-cask-pr",
"--version", version.to_s,
"--sha256", ":no_check",
"--message", "Automatic update via `brew bump-unversioned-casks`.",
cask.sourcefile_path
]

if dry_run
bump_cask_pr_args << "--dry-run"
oh1 "Would bump #{cask} from #{cask.version} to #{version}"
else
oh1 "Bumping #{cask} from #{cask.version} to #{version}"
end

begin
system_command! HOMEBREW_BREW_FILE, args: bump_cask_pr_args
rescue ErrorDuringExecution => e
onoe e
return
end
end
end
end

{
"sha256" => sha256,
"check_time" => check_time.iso8601,
"time" => time&.iso8601,
"file_size" => file_size,
}
end
end
21 changes: 16 additions & 5 deletions Library/Homebrew/download_strategy.rb
Expand Up @@ -356,7 +356,7 @@ def fetch

ohai "Downloading #{url}"

resolved_url, _, url_time = resolve_url_basename_time(url)
resolved_url, _, url_time, = resolve_url_basename_time_file_size(url)

fresh = if cached_location.exist? && url_time
url_time <= cached_location.mtime
Expand Down Expand Up @@ -398,14 +398,19 @@ def clear_cache
rm_rf(temporary_path)
end

def resolved_time_file_size
_, _, time, file_size = resolve_url_basename_time_file_size(url)
[time, file_size]
end

private

def resolved_url_and_basename
resolved_url, basename, = resolve_url_basename_time(url)
resolved_url, basename, = resolve_url_basename_time_file_size(url)
[resolved_url, basename]
end

def resolve_url_basename_time(url)
def resolve_url_basename_time_file_size(url)
@resolved_info_cache ||= {}
return @resolved_info_cache[url] if @resolved_info_cache.include?(url)

Expand Down Expand Up @@ -458,9 +463,15 @@ def resolve_url_basename_time(url)
.map { |t| t.match?(/^\d+$/) ? Time.at(t.to_i) : Time.parse(t) }
.last

file_size =
lines.map { |line| line[/^Content-Length:\s*(\d+)/i, 1] }
.compact
.map(&:to_i)
.last

basename = filenames.last || parse_basename(redirect_url)

@resolved_info_cache[url] = [redirect_url, basename, time]
@resolved_info_cache[url] = [redirect_url, basename, time, file_size]
end

def _fetch(url:, resolved_url:)
Expand Down Expand Up @@ -526,7 +537,7 @@ def combined_mirrors
@combined_mirrors = [*@mirrors, *backup_mirrors]
end

def resolve_url_basename_time(url)
def resolve_url_basename_time_file_size(url)
if url == self.url
super("#{apache_mirrors["preferred"]}#{apache_mirrors["path_info"]}")
else
Expand Down

0 comments on commit e416668

Please sign in to comment.