#!/usr/bin/ruby # APKBUILD dependency resolver for RubyGems # Copyright (C) 2014-2016 Kaarle Ritvanen require 'augeas' require 'optparse' require 'rubygems/dependency' require 'rubygems/resolver' require 'rubygems/spec_fetcher' class Package @@packages = {} def self.initialize level @@augeas = Augeas::open(nil, nil, Augeas::NO_MODL_AUTOLOAD) dir = Dir.pwd @@augeas.transform( :lens => 'Shellvars.lns', :incl => dir + '/*/ruby*/APKBUILD' ) @@augeas.load apath = '/files' + dir fail unless @@augeas.match("/augeas#{apath}//error").empty? repos = ['main', 'community', 'testing'] repos = repos[0..repos.index(level)] for repo in repos for pkg in @@augeas.match "#{apath}/#{repo}/*" Aport.new(pkg) unless pkg.end_with? '/ruby' end end Subpackage.initialize @@augeas.get("#{apath}/main/ruby/APKBUILD/pkgver") @@packages.each_value do |pkg| pkg.depends do |dep| dep.add_user pkg end end end def self.get name pkg = @@packages[name] raise 'Invalid package name: ' + name unless pkg pkg end def self.save fail unless @@augeas.save end def initialize name @name = name @depends = [] @users = [] @@packages[name] = self end def add_dependency name @depends << name end attr_reader :name def depends for dep in @depends # ruby-gems: workaround for v2.6 if dep.start_with?('ruby-') && dep != 'ruby-gems' unless @@packages.has_key? dep raise "Dependency for #{@name} does not exist: #{dep}" end yield @@packages[dep] end end end def users for user in @users yield user end end def add_user user @users << user end end class Aport < Package def initialize path super path.split('/')[-1] @path = path[6..-1] @apath = path + '/APKBUILD/' for dep in `echo #{get_param 'depends'}`.split add_dependency dep end end attr_reader :path def gem get_param '_gemname' end def version get_param 'pkgver' end def version= version set_param 'pkgver', version set_param 'pkgrel', '0' end def del_dependency name @depends.delete name set_param 'depends', "\"#{@depends.join ' '}\"" end private def get_param name value = @@augeas.get(@apath + name) raise name + ' not defined for ' + @name unless value value end def set_param name, value @@augeas.set(@apath + name, value) end end class Subpackage < Package RUBY_SUBPACKAGES = { '2.0.0_p353' => { 'ruby-minitest' => ['minitest', '4.3.2'], 'ruby-rake' => ['rake', '0.9.6'], 'ruby-rdoc' => ['rdoc', '4.0.0', 'ruby-json'] }, '2.0.0_p481' => { 'ruby-minitest' => ['minitest', '4.3.2'], 'ruby-rake' => ['rake', '0.9.6'], 'ruby-rdoc' => ['rdoc', '4.0.0', 'ruby-json'] }, '2.1.5' => { 'ruby-json' => ['json', '1.8.1'], 'ruby-minitest' => ['minitest', '4.7.5'], 'ruby-rake' => ['rake', '10.1.0'], 'ruby-rdoc' => ['rdoc', '4.1.0', 'ruby-json'] }, '2.2.1' => { # it's actually 0.4.3 but that version is not published on network 'ruby-io-console' => ['io-console', '0.4.2'], 'ruby-json' => ['json', '1.8.1'], 'ruby-minitest' => ['minitest', '5.4.3'], 'ruby-rake' => ['rake', '10.4.2'], 'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json'] }, '2.2.2' => { # it's actually 0.4.3 but that version is not published on network 'ruby-io-console' => ['io-console', '0.4.2'], 'ruby-json' => ['json', '1.8.1'], 'ruby-minitest' => ['minitest', '5.4.3'], 'ruby-rake' => ['rake', '10.4.2'], 'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json'] }, '2.2.3' => { # it's actually 0.4.3 but that version is not published on network 'ruby-io-console' => ['io-console', '0.4.2'], 'ruby-json' => ['json', '1.8.1'], 'ruby-minitest' => ['minitest', '5.4.3'], 'ruby-rake' => ['rake', '10.4.2'], 'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json'] }, '2.2.4' => { # it's actually 0.4.3 but that version is not published on network 'ruby-io-console' => ['io-console', '0.4.2'], 'ruby-json' => ['json', '1.8.1'], 'ruby-minitest' => ['minitest', '5.4.3'], 'ruby-rake' => ['rake', '10.4.2'], 'ruby-rdoc' => ['rdoc', '4.2.0', 'ruby-json'] } } @@subpackages = [] def self.initialize version for name, attrs in RUBY_SUBPACKAGES[version] new name, attrs end end def self.each for pkg in @@subpackages yield pkg end end def initialize name, attrs super name @gem, @version, *deps = attrs for dep in deps add_dependency dep end @@subpackages << self end attr_reader :gem, :version end class Update def initialize @gems = {} @deps = [] end def require_version name, version gem = assign(Package.get(name).gem, name) @deps << gem.dependency if gem.require_version version end def resolve for pkg in Subpackage require_version pkg.name, pkg.version unless @gems[pkg.gem] end def check_deps @gems.clone.each_value do |gem| gem.check_deps end end check_deps for req in Gem::Resolver.new(@deps).resolve spec = req.spec gem = @gems[spec.name] gem.require_version spec.version.version if gem end check_deps for name, gem in @gems if gem.updated? gem.package.users do |user| ugem = @gems[user.gem] if !ugem || ugem.package.name != user.name Gem::Resolver.new( [gem.dependency, Gem::Dependency.new(user.gem, user.version)] ).resolve end end end end end def each @gems.each_value do |gem| update = gem.update yield update if update end end def assign name, package pkg = Package.get package if @gems.has_key? name gem = @gems[name] return gem if pkg == gem.package raise "Conflicting packages for gem #{name}: #{gem.package.name} and #{pkg.name}" end gem = PackagedGem.new self, name, pkg @gems[name] = gem gem end private class PackagedGem def initialize update, name, package @update = update @name = name @package = package end attr_reader :package def require_version version if @version return false if version == @version raise "Conflicting versions for gem #{@name}: #{@version} and #{version}" end @version = version true end def version @version || @package.version end def updated? version != @package.version end def dependency Gem::Dependency.new(@name, version) end def check_deps specs, errors = Gem::SpecFetcher::fetcher.spec_for_dependency(dependency) raise "Invalid gem: #{@name}-#{version}" if specs.empty? fail if specs.length > 1 deps = specs[0][0].runtime_dependencies @obsolete_deps = [] @package.depends do |dep| gem = @update.assign(dep.gem, dep.name) gem.check_deps unless deps.reject! { |sdep| sdep.match? dep.gem, gem.version } @obsolete_deps << dep.name end end unless deps.empty? raise 'Undeclared dependencies in ' + @package.name + deps.inject('') { |s, dep| "#{s}\n#{dep.name} #{dep.requirements_list.join ' '}" } end end def update updated? || !@obsolete_deps.empty? ? ( { :name => @package.name, :version => version, :obsolete_deps => @obsolete_deps.clone, :path => @package.path } ) : nil end end end level = 'main' update_files = nil OptionParser.new do |opts| opts.on('-c', '--community') do |c| level = 'community' end opts.on('-t', '--testing') do |t| level = 'testing' end opts.on('-u', '--update') do |u| update_files = [] end end.parse! ARGV Package.initialize level latest = {} for source, gems in Gem::SpecFetcher::fetcher.available_specs(:latest)[0] for gem in gems latest[gem.name] = gem.version.version end end update = Update.new for arg in ARGV match = /^(([^-]|-[^\d])+)(-(\d.*))?/.match arg name = match[1] update.require_version name, match[4] || latest[Package.get(name).gem] end update.resolve for pkg in update obsolete = pkg[:obsolete_deps] obs = obsolete.empty? ? nil : " (obsolete dependencies: #{obsolete.join ', '})" puts "#{pkg[:name]}-#{pkg[:version]}#{obs}" if update_files package = Package.get(pkg[:name]) package.version = pkg[:version] for dep in obsolete package.del_dependency dep end update_files << pkg[:path] end end if update_files Package.save for path in update_files Dir.chdir(path) do fail unless system('abuild checksum') end end end