diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 0a19016..ef66d30 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -70,6 +70,7 @@ class Gem::CommandManager register_command :unpack register_command :update register_command :which + register_command :nix end ## diff --git a/lib/rubygems/commands/nix_command.rb b/lib/rubygems/commands/nix_command.rb new file mode 100644 index 0000000..005d5a9 --- /dev/null +++ b/lib/rubygems/commands/nix_command.rb @@ -0,0 +1,226 @@ +require 'net/http' +require 'rubygems/command' +require 'rubygems/doc_manager' +require 'rubygems/install_update_options' +require 'rubygems/dependency_installer' +require 'rubygems/local_remote_options' +require 'rubygems/validator' +require 'rubygems/exceptions' +require 'rubygems/version_option' +require 'rubygems/version' +require 'open3' + + +def nixname(gem) + s = "#{gem}" == gem ? gem : gem.full_name + s.gsub(/[.-]/,'_') +end + +def nixdescription(spec) + desc_from_spec = spec.description + desc = desc_from_spec.sub(/[.].*/,'') # only keep first sentence + desc = desc.length > 120 \ + ? "description = \"#{ desc[0..120] }\"; # cut to 120 chars" \ + : "description = \"#{ desc }\";" + desc = desc.sub(/";$/,"[...]\";") if desc != desc_from_spec + desc.gsub("\n"," ") # no \ns in description +end + +## +# tool creating nix expression to install gems (from ruby forge etc) +# +# this is work in progress + +class Gem::Commands::NixCommand < Gem::Command + + include Gem::VersionOption + include Gem::LocalRemoteOptions + include Gem::InstallUpdateOptions + + def initialize + defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ + }) + super 'nix', 'create a nix file containing expressions of the gems', defaults + end + + def description # :nodoc: + <<-EOF + create a nix file containing expressions of the gems + There are many gems. So it's best to only specify some target gems and + take them into acocunt with their deps + TODO more details + EOF + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...] [options] -- --build-flags" + end + + def arguments # :nodoc: + "GEMNAME name of gem to be added to the expressions file" + end + + def defaults_str # :nodoc: + # what to put in here ? TODO (probably nothing is ok) + "" + end + + def execute + + begin + @prerelease = false; + + args = options[:args]; + + @gems_with_deps = {} + @seen = {} + + # args to dep informations + args.map { |arg| + if arg =~ /(.+)-?(.*)?/ then + gem_name = $1 + version = $2.empty? ? Gem::Requirement.default : Gem::Version.new($2) + else + raise Gem::CommandLineError, "could'nt parse arg. expected: name or name-version" + end + + print "adding gem_name\n" + + adddep(Gem::Dependency.new gem_name, version) + } + + print " total: #{@gems_with_deps.length}\n" + + + out = " + # WARNING: automatically generated CODE + # This section has been generated by gem nix #{args.join(" ")} + # the gem nix command has been added by a nix patch to ruby gems + " + # define aliases + aliases = {} + @gems_with_deps.each{ |key, (spec, src, deps)| + aliases[spec.name] = spec \ + if aliases[spec.name].nil? || aliases[spec.name].version < spec.version + + src_url = "http://gems.rubyforge.org/gems/#{spec.full_name}.gem" + + # [> get true mirror url reading redirect contents + # h = Net::HTTP.new('gems.rubyforge.org', 80) + # resp, data = h.get("/gems/#{spec.full_name}.gem", nil) + # if resp.code == "200" then + # src_url = "http://gems.rubyforge.org/gems/#{spec.full_name}.gem" + # else if resp.code == "302" then + # src_url = resp['location'] + # print "redirection: http://gems.rubyforge.org/gems/#{spec.full_name}.gem -> #{src_url}\n" + # else + # raise Gem::DependencyError.new("unkown http return code #{resp} #{data}") + # end + # end + + #raise Gem::DependencyError("src_url is nil, 302 redirection failed?") if src_url.nil? + + out = " + #{out} + #{nixname spec} = rubyDerivation { + name = \"ruby-#{spec.full_name}\"; # full_name + nameNoVersion = \"#{nixname spec.name}\"; + propagatedBuildInputs = [ #{deps.map {|n| n.nil? ? "" : (nixname n) }.join(" ")} ]; + src = fetchurl { + url = \"#{src_url}\"; + sha256 = \"#{nixhashof src_url}\"; + }; + meta = { + homepage = \"#{spec.homepage}\"; + license = [#{spec.licenses.map{|l| "\"#{l}\""}.join(" ") }]; # one of ? + #{nixdescription spec} + longDescription = \"#{ spec.description }\"; + }; + };\n" + } + + out = "#{out}\n# aliases\n" + aliases.each { |key, spec | + out = "#{out}#{nixname key}=#{nixname spec};\n" + } + + print out + exit_code = 0 + + rescue => e + puts e.inspect + puts e.backtrace + end + + + end + + # helper funtions ================ + + def adddep(dep) + gem = find_gem_with_source(dep) + raise Gem::CommandLineError, "couldn't find #{dep}" if gem.nil? + full_name = gem[0].full_name + + return if @seen[full_name] + @seen[full_name] = true # there maybe circular dependencies. thus mark this gem seen as early as possible + + # distinguish runtime / buildtime deps? (TODO) + deps = gem[0].dependencies + + print " total deps of #{full_name}: #{deps.length}\n" + + dep_specs = [] + # recurse while collecting deps + deps.each {|dep_var| dep_specs.push(adddep(dep_var)) } + + + @gems_with_deps[full_name] = [ + gem[0], # spec + gem[1], # src + dep_specs # deps + ] + gem[0] # only return spec, no source for dep list + end + + + # copied from dependency_installer, stripped + def find_gem_with_source(dep) + gems_and_sources = [] + + # no local + + requirements = dep.version_requirements.requirements.map do |req, ver| + req + end + + all = true + found = Gem::SpecFetcher.fetcher.fetch dep, all, true, @prerelease + found.reverse[0] + end + + + def nixhashof(src) + cashfile="#{ENV['HOME']}/.nix-ruby-gems-cache" + cash = {} + if FileTest.exists?(cashfile) + File.open(cashfile,'r') do |f| Marshal.load(f) end + end + + if cash[src].nil? then + tmp="/tmp/ruby-gems-nix-tmp-file" + raise Gem::DependencyError("could'nt nix-prefetch #{src}") \ + if (not system("nix-prefetch-url #{src.gsub(/([:= `$;])/,'\\\\\1')} > #{tmp} 2>/dev/null")) || $? != 0 + file = File.new(tmp) + hash = file.readlines().first().split("\n")[0] # remove trailing \n + file.close() + File.delete(tmp) + cash[src] = hash + + File.open(cashfile, "w+") do |f| Marshal.dump(cash, f) end + end + + return cash[src] + end + +end