apkbuild-gem-resolver.in 6.96 KB
Newer Older
1 2 3 4 5 6
#!/usr/bin/ruby

# APKBUILD dependency resolver for RubyGems
# Copyright (C) 2014 Kaarle Ritvanen

require 'augeas'
7
require 'optparse'
8 9 10 11 12 13
require 'rubygems/dependency'
require 'rubygems/resolver'
require 'rubygems/spec_fetcher'

class Aport
  RUBY_SUBPACKAGES = {
14 15 16 17 18
    '2.0.0_p353' => {
      'ruby-minitest' => ['minitest', '4.3.2'],
      'ruby-rake' => ['rake', '0.9.6'],
      'ruby-rdoc' => ['rdoc', '4.0.0', 'ruby-json']
    },
19 20 21 22 23 24
    '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' => {
25
      'ruby-json' => ['json', '1.8.1'],
26 27 28
      'ruby-minitest' => ['minitest', '4.7.5'],
      'ruby-rake' => ['rake', '10.1.0'],
      'ruby-rdoc' => ['rdoc', '4.1.0', 'ruby-json']
29 30 31 32 33 34 35 36 37
    },
    '2.2.1' => {
      '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'],
      'ruby-io-console' => [ 'io-console', '0.4.2'] # its actually 0.4.3 but 
      						    # that version is not
						    # published on network
38
    }
39 40 41 42 43 44 45 46
    '2.2.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'],
      'ruby-io-console' => [ 'io-console', '0.4.2'] # its actually 0.4.3 but
						    # that version is not
						    # published on network
47 48 49 50 51
  }

  @@aports = {}
  @@subpackages = []

52
  def self.initialize testing
53 54 55 56 57 58 59 60
    Augeas::open(nil, nil, Augeas::NO_MODL_AUTOLOAD) do |aug|
      dir = Dir.pwd
      aug.transform(:lens => 'Shellvars.lns', :incl => dir + '/*/ruby*/APKBUILD')
      aug.load

      apath = '/files' + dir
      fail if aug.match("/augeas#{apath}//error").length > 0

61
      repos = ['main']
62
      repos << 'testing' if testing
63
      for repo in repos
64 65 66 67 68 69 70 71 72 73 74 75 76
        for aport in aug.match "#{apath}/#{repo}/*"
          FileAport.new(aug, aport) unless aport.end_with? '/ruby'
        end
      end

      for name, attrs in RUBY_SUBPACKAGES[
        aug.get("#{apath}/main/ruby/APKBUILD/pkgver")
      ]
        gem, version, *deps = attrs
        aport = new name, gem, version
        for dep in deps
          aport.add_dependency dep
        end
77
        @@subpackages << aport
78 79 80 81 82 83 84 85 86 87 88
      end
    end

    @@aports.each_value do |aport|
      aport.depends do |dep|
        dep.add_user aport
      end
    end
  end

  def self.get name
89 90 91
    aport = @@aports[name]
    raise 'Invalid package name: ' + name unless aport
    aport
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
  end

  def self.ruby_subpkgs
    for pkg in @@subpackages
      yield pkg
    end
  end

  def initialize name, gem, version
    @name = name
    @gem = gem
    @version = version
    @depends = []
    @users = []
    @@aports[name] = self
  end

  def add_dependency name
110
    @depends << name
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
  end

  attr_reader :gem, :name, :version

  def depends
    for dep in @depends
      unless @@aports.has_key? dep
        raise "Dependency for #{@name} does not exist: #{dep}"
      end
      yield @@aports[dep]
    end
  end

  def users
    for user in @users
      yield user
    end
  end

  def add_user user
131
    @users << user
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
  end
end

class FileAport < Aport
  def initialize aug, path
    name = path.split('/')[-1]

    get = proc{ |param|
      res = aug.get(path + '/APKBUILD/' + param)
      raise param + ' not defined for ' + name unless res
      res
    }

    super name, get.call('_gemname'), get.call('pkgver')

    for dep in `echo #{get.call('depends')}`.split
148 149
      # ruby-gems: workaround for v2.6
      add_dependency dep if dep.start_with?('ruby-') && dep != 'ruby-gems'
150 151 152 153 154 155 156 157 158 159 160 161
    end
  end
end


class Update
  def initialize 
    @gems = {}
    @deps = []
  end

  def require_version name, version
162
    gem = assign(Aport.get(name).gem, name)
163
    @deps << gem.dependency if gem.require_version version
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
  end

  def resolve
    Aport.ruby_subpkgs do |pkg|
      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.aport.users do |user|
          ugem = @gems[user.gem]
          if !ugem || ugem.aport.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|
      obs = gem.obsolete_deps
      obs = obs.length == 0 ? nil : " (obsolete dependencies: #{obs.join ', '})"

      if gem.updated? || obs
        yield "#{gem.aport.name}-#{gem.version}#{obs}"
      end
    end
  end

  def assign name, aport
    aport = Aport.get aport

    if @gems.has_key? name
      gem = @gems[name]
      return gem if aport == gem.aport
      raise "Conflicting packages for gem #{name}: #{gem.aport.name} and #{aport.name}"
    end

    gem = AportGem.new self, name, aport
    @gems[name] = gem
    gem
  end

  private

  class AportGem
    def initialize update, name, aport
      @update = update
      @name = name
      @aport = aport
    end

    attr_reader :aport, :obsolete_deps

    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 || @aport.version
    end

    def updated?
      version != @aport.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.length == 0
      fail if specs.length > 1
      deps = specs[0][0].runtime_dependencies

      @obsolete_deps = []

      @aport.depends do |dep|
        gem = @update.assign(dep.gem, dep.name)
        gem.check_deps
        unless deps.reject! { |sdep| sdep.match? dep.gem, gem.version }
270
          @obsolete_deps << dep.name
271 272 273 274 275 276 277 278 279 280 281 282 283
        end
      end

      if deps.length > 0
        raise 'Undeclared dependencies in ' + @aport.name + deps.inject('') {
          |s, dep| "#{s}\n#{dep.name} #{dep.requirements_list.join ' '}"
        }
      end
    end
  end
end


284 285 286 287 288 289 290 291
testing = false
OptionParser.new do |opts|
  opts.on('-t', '--testing') do |t|
    testing = t
  end
end.parse! ARGV
Aport.initialize testing

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
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[Aport.get(name).gem]
end

update.resolve

for aport in update
  puts aport
end