#!/usr/bin/env ruby
# frozen_string_literal: true
require 'uri'
require 'net/http'
require 'openssl'
require 'rubygems/version'
require 'fspath'
CONFIG = {
advancecomp: {
url: 'https://github.com/amadvance/advancecomp/releases.atom',
regexp: %r{tag:github.com,2008:Repository/\d+/\D+?(\d+(?:\.\d+)*)},
info_url: 'http://www.advancemame.it/comp-history',
},
gifsicle: {
url: 'https://www.lcdf.org/gifsicle/',
regexp: /gifsicle-(\d+(?:\.\d+)*)\.tar\.gz/,
info_url: 'https://www.lcdf.org/gifsicle/changes.html',
},
jhead: {
url: 'https://www.sentex.ca/~mwandel/jhead/',
regexp: /jhead-(\d+(?:\.\d+)*)/,
info_url: 'https://www.sentex.ca/~mwandel/jhead/changes.txt',
},
jpegoptim: {
url: 'https://github.com/tjko/jpegoptim/tags.atom',
regexp: %r{tag:github.com,2008:Repository/\d+/\D+?(\d+(?:\.\d+)*)},
info_url: 'https://github.com/tjko/jpegoptim/#readme',
},
jpegarchive: {
url: 'https://github.com/danielgtaylor/jpeg-archive/releases.atom',
regexp: %r{tag:github.com,2008:Repository/\d+/\D+?(\d+(?:\.\d+)*)},
info_url: 'https://github.com/danielgtaylor/jpeg-archive/releases',
},
libjpeg: {
url: 'https://www.ijg.org/files/',
regexp: /jpegsrc.v(.*?).tar.gz/,
info_url: 'https://jpegclub.org/reference/reference-sources/#:~:text=CHANGE%20LOG',
},
liblcms: {
url: 'https://sourceforge.net/projects/lcms/rss?path=/lcms',
regexp: %r{/lcms/(\d+(?:\.\d+)*)/},
info_url: 'https://www.littlecms.com/blog/',
},
libmozjpeg: {
url: 'https://github.com/mozilla/mozjpeg/releases.atom',
regexp: %r{tag:github.com,2008:Repository/\d+/\D+?(\d+(?:\.\d+)*)},
info_url: 'https://github.com/mozilla/mozjpeg/releases',
},
libpng: {
url: 'https://sourceforge.net/projects/libpng/rss?path=/libpng16',
regexp: %r{/libpng\d*/(\d+(?:\.\d+)*)/},
info_url: 'https://github.com/glennrp/libpng/blob/libpng16/CHANGES#:~:text=Send%20comments/corrections/commendations%20to%20png%2Dmng%2Dimplement%20at%20lists.sf.net.',
},
libz: {
url: 'https://sourceforge.net/projects/libpng/rss?path=/zlib',
regexp: %r{/zlib/(\d+(?:\.\d+)*)/},
info_url: 'https://github.com/glennrp/zlib/blob/master/ChangeLog',
},
optipng: {
url: 'https://sourceforge.net/projects/optipng/rss',
regexp: %r{/OptiPNG/optipng-(\d+(?:\.\d+)*)/},
info_url: 'https://optipng.sourceforge.net/history.txt',
},
oxipng: {
url: 'https://github.com/shssoichiro/oxipng/releases.atom',
regexp: %r{tag:github.com,2008:Repository/\d+/v(\d+(?:\.\d+)*)},
info_url: 'https://github.com/shssoichiro/oxipng/releases',
},
pngcrush: {
url: 'https://sourceforge.net/projects/pmt/rss?path=/pngcrush',
regexp: %r{/pngcrush/(\d+(?:\.\d+)*)/},
info_url: 'https://pmt.sourceforge.io/pngcrush/ChangeLog.html',
},
pngout: {
url: 'http://www.jonof.id.au/kenutils.html',
regexp: %r{/files/kenutils/pngout-(\d{8})-linux.tar.gz},
info_url: 'http://www.jonof.id.au/kenutils.html#:~:text=Revision%20history',
},
pngquant: {
url: 'https://pngquant.org/releases.html',
regexp: %r{(\d+(?:\.\d+)*)},
},
}.freeze
# Fetch and compare latest tool/library versions
class Livecheck
# Commands
class Cli
VERSION_REGEXP = /^([A-Z]+)_VER *:= *(.*)$/.freeze
def initialize(args)
@update = args.delete('--update')
abort '--update is the only supported option' unless args.empty?
end
def run
dump_changes
return unless livechecks.any?(&:changed?)
exit 1 unless @update
update_changelog
update_makefile
rescue StandardError => e
warn e
exit 2
end
private
def dump_changes
livechecks.each do |lc|
next unless lc.changed?
puts "#{lc.name_n_latest_version} (current: #{lc.current_version}) #{lc.info_url}"
end
end
def update_changelog
changelog = FSPath('CHANGELOG.markdown')
lines = changelog.readlines
lines.insert(4, "\n") if lines[4] =~ /^## /
lines.insert(4, changelog_entry)
write(changelog, lines.join(''))
end
def update_makefile
content = makefile.read.gsub(VERSION_REGEXP) do
livecheck = livechecks_by_name[Regexp.last_match[1].downcase]
"#{livecheck.name.upcase}_VER := #{livecheck.latest_version}"
end
write(makefile, content)
end
def makefile
FSPath('Makefile')
end
def livechecks
@livechecks ||= makefile.read.scan(VERSION_REGEXP).map do |name, version|
Livecheck.new(name.downcase, version)
end
end
def livechecks_by_name
@livechecks_by_name ||= Hash[livechecks.map{ |lc| [lc.name, lc] }]
end
def changelog_entry
github_user = `git config github.user`.strip
changed = livechecks.select(&:changed?)
"* #{to_sentence(changed.map(&:name_n_latest_version))} [@#{github_user}](https://github.com/#{github_user})\n"
end
def to_sentence(array)
case array.length
when 0 then ''
when 1 then array[0].to_s
else "#{array[0...-1].join(', ')} and #{array[-1]}"
end
end
def write(path, data)
path.temp_file(path.dirname) do |io|
io.write data
mode = path.exist? ? path.stat.mode : (~File.umask & 0o777)
io.path.rename(path)
path.chmod(mode)
end
warn "Wrote #{path}"
end
end
# Compare versions including libjpeg ones (9b <=> 9)
class Version
include Comparable
attr_reader :string, :parsed
alias_method :to_s, :string
def initialize(string)
@string = string
@parsed = begin
Gem::Version.new(string)
rescue ArgumentError
nil
end
end
def <=>(other)
if parsed && other.parsed
parsed <=> other.parsed
else
string <=> other.string
end
end
end
attr_reader :name, :current_version
def initialize(name, current_version)
@name = name
@current_version = Version.new(current_version)
@fetcher = Thread.new{ fetch_versions.last }
end
def latest_version
@fetcher.value
end
def changed?
latest_version != current_version
end
def name_n_latest_version
"#{name} #{latest_version}"
end
def info_url
config[:info_url]
end
private
def config
CONFIG[name.to_sym] || fail(ArgumentError, "Livecheck for #{name} not defined")
end
def get(url)
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.request_get(uri.request_uri).body
end
def fetch_versions
body = get(config[:url])
version_regex = config[:regexp]
versions = body.scan(version_regex).map{ |match| Version.new(*match) }.sort
fail "No versions found for #{name} in body:\n#{body}" if versions.empty?
versions
end
end
Livecheck::Cli.new(ARGV).run