apkbuild-gem-resolver.in 7.21 KB
Newer Older
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
1 2 3
#!/usr/bin/ruby

# APKBUILD dependency resolver for RubyGems
4
# Copyright (C) 2014-2015 Kaarle Ritvanen
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
5 6

require 'augeas'
7
require 'optparse'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
8 9 10 11
require 'rubygems/dependency'
require 'rubygems/resolver'
require 'rubygems/spec_fetcher'

12 13
class Package
  @@packages = {}
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
14

15
  def self.initialize testing
16 17 18 19 20 21 22 23
    @@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
24
    fail unless @@augeas.match("/augeas#{apath}//error").empty?
25 26 27 28 29 30

    repos = ['main']
    repos << 'testing' if testing
    for repo in repos
      for pkg in @@augeas.match "#{apath}/#{repo}/*"
        Aport.new(pkg) unless pkg.end_with? '/ruby'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
31 32 33
      end
    end

34 35
    Subpackage.initialize @@augeas.get("#{apath}/main/ruby/APKBUILD/pkgver")

36 37 38
    @@packages.each_value do |pkg|
      pkg.depends do |dep|
        dep.add_user pkg
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
39 40 41 42 43
      end
    end
  end

  def self.get name
44 45 46
    pkg = @@packages[name]
    raise 'Invalid package name: ' + name unless pkg
    pkg
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
47 48
  end

49
  def initialize name
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
50 51 52
    @name = name
    @depends = []
    @users = []
53
    @@packages[name] = self
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
54 55 56
  end

  def add_dependency name
57
    @depends << name
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
58 59
  end

60
  attr_reader :name
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
61 62 63

  def depends
    for dep in @depends
64
      unless @@packages.has_key? dep
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
65 66
        raise "Dependency for #{@name} does not exist: #{dep}"
      end
67
      yield @@packages[dep]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
68 69 70 71 72 73 74 75 76 77
    end
  end

  def users
    for user in @users
      yield user
    end
  end

  def add_user user
78
    @users << user
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
79 80 81
  end
end

82
class Aport < Package
83
  def initialize path
84 85
    super path.split('/')[-1]
    @path = path + '/APKBUILD/'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
86

87
    for dep in `echo #{get_param 'depends'}`.split
88 89
      # ruby-gems: workaround for v2.6
      add_dependency dep if dep.start_with?('ruby-') && dep != 'ruby-gems'
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
90 91
    end
  end
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107

  def gem
    get_param '_gemname'
  end

  def version
    get_param 'pkgver'
  end

  private

  def get_param name
    value = @@augeas.get(@path + name)
    raise name + ' not defined for ' + @name unless value
    value
  end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
108 109
end

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
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']
    }
  }

  @@subpackages = []

  def self.initialize version
    for name, attrs in RUBY_SUBPACKAGES[version]
150
      new name, attrs
151 152 153 154 155 156 157 158
    end
  end

  def self.each
    for pkg in @@subpackages
      yield pkg
    end
  end
159 160 161 162 163 164 165 166 167 168 169

  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
170 171
end

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
172 173 174 175 176 177 178 179

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

  def require_version name, version
180
    gem = assign(Package.get(name).gem, name)
181
    @deps << gem.dependency if gem.require_version version
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
182 183 184
  end

  def resolve
185
    for pkg in Subpackage
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
      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?
207
        gem.package.users do |user|
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
208
          ugem = @gems[user.gem]
209
          if !ugem || ugem.package.name != user.name
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
210 211 212 213 214 215 216 217 218 219 220
            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|
221 222
      update = gem.update
      yield update if update
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
223 224 225
    end
  end

226 227
  def assign name, package
    pkg = Package.get package
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
228 229 230

    if @gems.has_key? name
      gem = @gems[name]
231 232
      return gem if pkg == gem.package
      raise "Conflicting packages for gem #{name}: #{gem.package.name} and #{pkg.name}"
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
233 234
    end

235
    gem = PackagedGem.new self, name, pkg
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
236 237 238 239 240 241
    @gems[name] = gem
    gem
  end

  private

242 243
  class PackagedGem
    def initialize update, name, package
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
244 245
      @update = update
      @name = name
246
      @package = package
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
247 248
    end

249
    attr_reader :package
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
250 251 252 253 254 255 256 257 258 259 260

    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
261
      @version || @package.version
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
262 263 264
    end

    def updated?
265
      version != @package.version
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
266 267 268 269 270 271 272 273
    end

    def dependency
      Gem::Dependency.new(@name, version)
    end

    def check_deps
      specs, errors = Gem::SpecFetcher::fetcher.spec_for_dependency(dependency)
274
      raise "Invalid gem: #{@name}-#{version}" if specs.empty?
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
275 276 277 278 279
      fail if specs.length > 1
      deps = specs[0][0].runtime_dependencies

      @obsolete_deps = []

280
      @package.depends do |dep|
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
281 282 283
        gem = @update.assign(dep.gem, dep.name)
        gem.check_deps
        unless deps.reject! { |sdep| sdep.match? dep.gem, gem.version }
284
          @obsolete_deps << dep.name
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
285 286 287
        end
      end

288
      unless deps.empty?
289
        raise 'Undeclared dependencies in ' + @package.name + deps.inject('') {
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
290 291 292 293
          |s, dep| "#{s}\n#{dep.name} #{dep.requirements_list.join ' '}"
        }
      end
    end
294 295

    def update
296
      updated? || !@obsolete_deps.empty? ? (
297
        {
298
          :name => @package.name,
299 300 301 302 303
          :version => version,
          :obsolete_deps => @obsolete_deps.clone
        }
      ) : nil
    end
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
304 305 306 307
  end
end


308 309 310 311 312 313
testing = false
OptionParser.new do |opts|
  opts.on('-t', '--testing') do |t|
    testing = t
  end
end.parse! ARGV
314
Package.initialize testing
315

Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
316 317 318 319 320 321 322 323 324 325 326
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]
327
  update.require_version name, match[4] || latest[Package.get(name).gem]
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
328 329 330 331
end

update.resolve

332 333
for pkg in update
  obs = pkg[:obsolete_deps]
334
  obs = obs.empty? ? nil : " (obsolete dependencies: #{obs.join ', '})"
335 336

  puts "#{pkg[:name]}-#{pkg[:version]}#{obs}"
Kaarle Ritvanen's avatar
Kaarle Ritvanen committed
337
end