#!/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