Gem 看上去 GemBundler 并没有解决版本冲突的问题啊

ibachue · 2012年10月15日 · 最后由 iBachue 回复于 2014年01月15日 · 5801 次阅读

大家好, 最近在研究 GemBundler,一直觉得用了 GemBundler 之后 Gem 版本发生冲突的概率大幅减小了,比以前在 config/environment.rb 中 config.gem 的办法强多了,于是一直希望找到 GemBundler 解决版本冲突的代码,但是至今无果。。。 然后我又试图做个试验来找到这段代码,但是在实验中,我震惊了: 从图中看到,我做了两个 gem,分别依赖 json >= 1.7.3 和 json <= 1.7.4,当前 rubygems.org 上 json 的最新版本是 1.7.5,然后我创建了一个 Rails 项目,导入我自己做的这两个 gem,然后在 Gemfile 中同时声明他们,运行 bundle install,出现了上述错误,GemBundler 竟然认为我做的那个依赖 json >= 1.7.3 的 gem 依赖的是 1.7.5?因而不能解决?天哪! 毫无疑问,json >= 1.7.3 和 json <= 1.7.4 的版本冲突是完全可以解决的,只要取 json 1.7.4 即可,但是 GemBundler 却不能解决它们。 本来想证明 GemBundler 在解决版本冲突上有更好的解决方案,但实验结果恰恰推翻了这个证明。那么为什么感觉用了 GemBundler 之后 Gem 版本发生冲突的概率小了呢?难道是我的幻觉? 实在不能理解,故请教大家。谢谢!

根据 http://gembundler.com/v1.2/man/bundle-update.1.html

If bundle install(1) reports a conflict, manually update the specific gems that you changed in the Gemfile(5)

试试 bundle update? 我没有测试验证过,看文档似乎可行。

#1 楼 @5long 不行啊 还是不能解决

#2 楼 @iBachue 你那不废话嘛,一个要求版本 <=x.x.x , 另一个要求版本 >=x.x.x +1 , 肯定是空集,要是我,也给你报错,说明两个 gem 水火不容。

#2 楼 @iBachue 好吧,那我只能继续猜:

bundler 之所以会拿 1.7.5 这个版本来用,说明这个版本信息一定是从系统某处读取出来的 (反正不是这两个 gem 的 gemspec). 目前来看只能是来自 Gemfile.lockgem list.

设法把这两个信息清除掉 (亦即,分别删除 Gemfile.lock 和已经安装的 json gem), 应该就不会干扰 bundler 的解析了。

这样要求就相当于要一个 Ruby VM 同时存在两个不同的 A 类

#3 楼 @lyfi2003 额 你“>=x.x.x +1”那个+1 是哪里来的呢?

#4 楼 @5long 哎 都已经试过了 我现在在看 GemBundler 检测冲突的代码 希望能从里面发现问题

#5 楼 @cxh116 不是啊 我要求 json >= 1.7.3 && json <= 1.7.4,这个不是空集啊。

json >= 1.7.3 && json <= 1.7.4 到底是 1.7.3 还是 1.7.4,二选一你自己能回答吗? 新建目录,里面只有 Gemfile 内容: gem json, '>=1.7.3' gem json, '<=1.7.4' 执行 bundle 报错 "You cannot specify the same gem twice with different version requirements. You specified: json (>= 1.7.3) and json (<= 1.7.4)"

bundler 能解决大部分版本冲突的问题。你这么故意玩它,表示伤不起~

#9 楼 @leekelby 不是啊 不能在 Gemfile 里定义两个名字相同版本号不同的 gem,也不能定义两个名字相同但是源不同的 gem,这个特性是显而易见的,而且在一个 Gemfile 里显式的声明两个名字相同的 gem 本身就是蛋疼行为,和我的问题无关。

# if there's already a dependency with this name we try to prefer one
if current = @dependencies.find { |d| d.name == dep.name }
  if current.requirement != dep.requirement
    if current.type == :development
      @dependencies.delete current
    elsif dep.type == :development
      return
    else
      raise DslError, "You cannot specify the same gem twice with different version requirements. " \
                      "You specified: #{current.name} (#{current.requirement}) and " \
                      "#{dep.name} (#{dep.requirement})"
    end
  end

  if current.source != dep.source
    if current.type == :development
      @dependencies.delete current
    elsif dep.type == :development
      return
    else
      raise DslError, "You cannot specify the same gem twice coming from different sources. You " \
                      "specified that #{dep.name} (#{dep.requirement}) should come from " \
                      "#{current.source || 'an unspecified source'} and #{dep.source}"
    end
  end
end

#10 楼 @iBachue Sorry, 我看错了。

不过关于声明版本不一致的时候,bundle 不会智能地找到最合适的版本,而是根据 Gemfile.lock 直接加载,如果 Gemfile.lock 里面出现两个不同版本,会提醒出来,( 就算使用 >= <= ), 解决方法只能删掉 lock 重新 bundle install

可以贴一下 Gemfile.lock 么

我也脚得应该自动解决

#11 楼 @lyfi2003 #12 楼 @hooopo 哦 我每次试验前有意先删除 Gemfile.lock 的 之前读代码就发现这个文件会影响很多行为,所以干脆先删掉了。 然后我发现,json >= 1.7.3 和 json <= 1.7.4 不能相容,但如果是 json >= 0.4.0 和 json <= 1.7.4 是能够相容的,但如果是 json >= 0.4.1 和 json <= 1.7.4 又变成不能相容了,0.4.0 的界限真的很奇怪,于是我重点研究 0.4.0 和 0.4.1 到底区别在什么地方。 随着阅读的深入,我发现其实 resolver 的 resolve instance method 是实现检查版本冲突的核心方法,但是实现非常麻烦,循环+递归,很难调试,所以我输出了一些信息,然后瞎猜了。以下内容均势我根据输出信息的猜测: 下面要输出的是调用 resolve 方法的参数,以及里面一个排序方法在排序前和排序后的数据对比,以及排序方法的 block 里返回的数据: 下面是 json >= 0.4.1 的输出信息:

➜ 6  /tmp/test-project  rm Gemfile.lock; bundle install 
rm: Gemfile.lock: No such file or directory
fatal: Not a git repository (or any of the parent directories): .git
fatal: Not a git repository (or any of the parent directories): .git
Fetching gem metadata from https://rubygems.org/..


resolve([use_json (>= 0) ruby, use_json_lower_than_1_7_4 (>= 0) ruby], {})
reqs before sort: [use_json (>= 0) ruby, use_json_lower_than_1_7_4 (>= 0) ruby]
use_json (>= 0) ruby => [1, 1, 1, 1]
use_json_lower_than_1_7_4 (>= 0) ruby => [1, 1, 1, 1]
reqs after sort: [use_json (>= 0) ruby, use_json_lower_than_1_7_4 (>= 0) ruby]


resolve([use_json_lower_than_1_7_4 (>= 0) ruby, json (>= 0.4.1) ruby], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>]})
reqs before sort: [use_json_lower_than_1_7_4 (>= 0) ruby, json (>= 0.4.1) ruby]
use_json_lower_than_1_7_4 (>= 0) ruby => [1, 1, 1, 1]
json (>= 0.4.1) ruby => [1, 1, 1, 49]
reqs after sort: [use_json_lower_than_1_7_4 (>= 0) ruby, json (>= 0.4.1) ruby]


resolve([json (>= 0.4.1) ruby, json (<= 1.7.4) ruby], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>], "use_json_lower_than_1_7_4"=>[#<Gem::Specification name=use_json_lower_than_1_7_4 version=0.0.1>]})
reqs before sort: [json (>= 0.4.1) ruby, json (<= 1.7.4) ruby]
json (>= 0.4.1) ruby => [1, 1, 1, 49]
json (<= 1.7.4) ruby => [1, 1, 1, 49]
reqs after sort: [json (>= 0.4.1) ruby, json (<= 1.7.4) ruby]


resolve([json (<= 1.7.4) ruby], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>], "use_json_lower_than_1_7_4"=>[#<Gem::Specification name=use_json_lower_than_1_7_4 version=0.0.1>], "json"=>[#<Gem::Specification name=json version=1.7.5>, #<Gem::Specification name=json version=1.7.5>]})
reqs before sort: [json (<= 1.7.4) ruby]
json (<= 1.7.4) ruby => [0, 1, 1, 0]
reqs after sort: [json (<= 1.7.4) ruby]
Bundler could not find compatible versions for gem "json":
  In Gemfile:
    use_json_lower_than_1_7_4 (>= 0) ruby depends on
      json (<= 1.7.4) ruby

    use_json (>= 0) ruby depends on
      json (1.7.5)

然后是 json >= 0.4.0 的输出信息:

➜ 6  /tmp/test-project  rm Gemfile.lock; bundle install 
rm: Gemfile.lock: No such file or directory
fatal: Not a git repository (or any of the parent directories): .git
fatal: Not a git repository (or any of the parent directories): .git
Fetching gem metadata from https://rubygems.org/..


resolve([use_json (>= 0) ruby, use_json_lower_than_1_7_4 (>= 0) ruby], {})
reqs before sort: [use_json (>= 0) ruby, use_json_lower_than_1_7_4 (>= 0) ruby]
use_json (>= 0) ruby => [1, 1, 1, 1]
use_json_lower_than_1_7_4 (>= 0) ruby => [1, 1, 1, 1]
reqs after sort: [use_json (>= 0) ruby, use_json_lower_than_1_7_4 (>= 0) ruby]


resolve([use_json_lower_than_1_7_4 (>= 0) ruby, json (>= 0.4.0) ruby], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>]})
reqs before sort: [use_json_lower_than_1_7_4 (>= 0) ruby, json (>= 0.4.0) ruby]
use_json_lower_than_1_7_4 (>= 0) ruby => [1, 1, 1, 1]
json (>= 0.4.0) ruby => [1, 1, 1, 50]
reqs after sort: [use_json_lower_than_1_7_4 (>= 0) ruby, json (>= 0.4.0) ruby]


resolve([json (>= 0.4.0) ruby, json (<= 1.7.4) ruby], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>], "use_json_lower_than_1_7_4"=>[#<Gem::Specification name=use_json_lower_than_1_7_4 version=0.0.1>]})
reqs before sort: [json (>= 0.4.0) ruby, json (<= 1.7.4) ruby]
json (>= 0.4.0) ruby => [1, 1, 1, 50]
json (<= 1.7.4) ruby => [1, 1, 1, 49]
reqs after sort: [json (<= 1.7.4) ruby, json (>= 0.4.0) ruby]


resolve([json (>= 0.4.0) ruby], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>], "use_json_lower_than_1_7_4"=>[#<Gem::Specification name=use_json_lower_than_1_7_4 version=0.0.1>], "json"=>[#<Gem::Specification name=json version=1.7.4>, #<Gem::Specification name=json version=1.7.4>]})
reqs before sort: [json (>= 0.4.0) ruby]
json (>= 0.4.0) ruby => [0, 1, 1, 0]
reqs after sort: [json (>= 0.4.0) ruby]


resolve([], {"use_json"=>[#<Gem::Specification name=use_json version=0.0.1>], "use_json_lower_than_1_7_4"=>[#<Gem::Specification name=use_json_lower_than_1_7_4 version=0.0.1>], "json"=>[#<Gem::Specification name=json version=1.7.4>, #<Gem::Specification name=json version=1.7.4>]})
Using json (1.7.4) 
Using use_json (0.0.1) from source at vendor/gems/use_json-0.0.1/ 
Using use_json_lower_than_1_7_4 (0.0.1) from source at vendor/gems/use_json_lower_than_1_7_4-0.0.1/ 
Using bundler (1.2.1) 
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.

注意它们的区别在于,如果是 json >= 0.4.0 的时候,json >= 0.4.0 在 json <= 1.7.4 之后被解析,而 json >= 0.4.1 的时候,json >= 0.4.1 在 json <= 1.7.4 之前被解析。如果 json <= 1.7.4 先被解析,则会向数组放进一个 json = 1.7.4 的 gem,这样,后来的 json >= 0.4.0 就能和它兼容了,但是如果是 json >= 0.4.1 先被解析,则数组中直接被写入了 json 的最新版本 1.7.5,这样再解析 json <= 1.7.4 的时候就会出错了。那决定解析顺序的代码是什么呢?

reqs = reqs.sort_by do |a|
  arr = [ activated[a.name] ? 0 : 1,
    a.requirement.prerelease? ? 0 : 1,
    @errors[a.name]   ? 0 : 1,
    activated[a.name] ? 0 : @gems_size[a] ]
  puts "#{a.inspect} => #{arr.inspect}"
  arr
end

这里我拦截这个数组,输出它后再返回它(源代码里就是直接返回这个数组)。可以从上门的输出信息中看到,真正决定谁先谁后的数据是数组最后一个元素,代码是@gems_size[a],通过阅读源码,发现 gems_size 的意思是,在 source 中符合这个 gem 要求的 gem 数量,在 rubygems.org 中,json >= 0.4.0 的 gem 有 50 个,json >= 0.4.1 的 gem 有 49 个,而 gem <= 1.7.4 的 gem 也是 49 个,由于 json >= 0.4.1 的 gem 数量等于 gem <= 1.7.4 的 gem 数量,在排序时没有优势,因此 json >= 0.4.1 先解析,然后出错了。而 json >= 0.4.0 的 gem 数量比 json <= 1.7.4 的 gem 数量多 1,在排序时有优势,所以后者前解析,成功。 这里最让人无语的是,source 里符合要求的 gem 数量竟然能影响 GemBundler 解决版本冲突的因素之一。我现在不知道该如何评论了。。。大家看看把??

#14 楼 @iBachue cool! 有钻研劲~ 这样看来应该是 bundler 的 bug 了,版本号中 A.B.Z 大版本 A 相同则兼容 (只是约定).

#15 楼 @lyfi2003 OK, 已经发 issue 了

#15 楼 @lyfi2003 今天 RubyConf 吃饭时遇到的还真的是你啊,很高兴见到你哦

好吧,历经了整整一年,这个 Bug 终于 Fix 了,还是在 1.6 的预览版本里,内流满面啊。https://github.com/bundler/bundler/issues/2122

需要 登录 后方可回复, 如果你还没有账号请 注册新账号