Last active
January 27, 2016 21:46
-
-
Save manveru/98d779e92ec4b9c02ae2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'bundler' | |
require 'json' | |
require 'open-uri' | |
require 'open3' | |
require 'pp' | |
class Bundix | |
VERSION = '2.0.0' | |
NIX_INSTANTIATE = 'nix-instantiate' | |
NIX_PREFETCH_URL = 'nix-prefetch-url' | |
NIX_PREFETCH_GIT = 'nix-prefetch-git' | |
NIX_HASH = 'nix-hash' | |
SHA256_32 = %r(^[a-z0-9]{52}$) | |
SHA256_16 = %r(^[a-f0-9]{64}$) | |
attr_reader :options | |
def initialize(options) | |
@options = { | |
gemset: './gemset.nix', | |
lockfile: './Gemfile.lock', | |
quiet: false, | |
tempfile: nil, | |
deps: false | |
}.merge(options) | |
end | |
def convert | |
cache = parse_gemset | |
lock = parse_lockfile | |
# reverse so git comes last | |
lock.specs.reverse_each.with_object({}) do |spec, gems| | |
name, cached = cache.find{|k,v| | |
k == spec.name && | |
v['version'] == spec.version.to_s && | |
v.dig('source', 'sha256').to_s.size == 52 | |
} | |
if cached | |
gems[name] = cached | |
next | |
end | |
gems[spec.name] = { | |
version: spec.version.to_s, | |
source: Source.new(spec).convert | |
} | |
if options[:deps] && spec.dependencies.any? | |
gems[spec.name][:dependencies] = spec.dependencies.map(&:name) - ['bundler'] | |
end | |
end | |
end | |
def parse_gemset | |
return {} unless File.file?(options[:gemset]) | |
json = Bundix.sh(NIX_INSTANTIATE, '--eval', '-E', | |
"builtins.toJSON(import #{options[:gemset]})") | |
JSON.parse(json.strip.gsub(/\\"/, '"')[1..-2]) | |
end | |
def parse_lockfile | |
Bundler::LockfileParser.new(File.read(options[:lockfile])) | |
end | |
def self.object2nix(obj, level = 2, out = '') | |
case obj | |
when Hash | |
out << "{\n" | |
obj.each do |k, v| | |
out << ' ' * level | |
if k.to_s =~ /^[a-zA-Z_-]+[a-zA-Z0-9_-]*$/ | |
out << k.to_s | |
else | |
object2nix(k, level + 2, out) | |
end | |
out << ' = ' | |
object2nix(v, level + 2, out) | |
out << (v.is_a?(Hash) ? "\n" : ";\n") | |
end | |
out << (' ' * (level - 2)) << (level == 2 ? '}' : '};') | |
when Array | |
out << '[' << obj.map{|o| o.to_str.dump }.join(' ') << ']' | |
when String | |
out << obj.dump | |
when Symbol | |
out << obj.to_s.dump | |
when true, false | |
out << obj.to_s | |
else | |
fail obj.inspect | |
end | |
end | |
def self.sh(*args) | |
out, status = Open3.capture2e(*args) | |
unless status.success? | |
puts "$ #{args.join(' ')}" if $VERBOSE | |
puts out if $VERBOSE | |
fail "command execution failed: #{status}" | |
end | |
out | |
end | |
class Source < Struct.new(:spec) | |
def convert | |
case spec.source | |
when Bundler::Source::Rubygems | |
convert_rubygems | |
when Bundler::Source::Git | |
convert_git | |
else | |
pp spec | |
fail 'unkown bundler source' | |
end | |
end | |
def sh(*args) | |
Bundix.sh(*args) | |
end | |
def nix_prefetch_url(*args) | |
sh(NIX_PREFETCH_URL, '--type', 'sha256', *args) | |
end | |
def nix_prefetch_git(uri, revision) | |
home = ENV['HOME'] | |
ENV['HOME'] = '/homeless-shelter' | |
sh(NIX_PREFETCH_GIT, '--url', uri, '--rev', revision, '--hash', 'sha256', '--leave-dotGit') | |
ensure | |
ENV['HOME'] = home | |
end | |
def fetch_local_hash(spec) | |
spec.source.caches.each do |cache| | |
path = File.join(cache, "#{spec.name}-#{spec.version}.gem") | |
next unless File.file?(path) | |
begin | |
return nix_prefetch_url("file://#{path}")[SHA256_32] | |
rescue | |
end | |
end | |
nil | |
end | |
def fetch_remotes_hash(spec, remotes) | |
remotes.each do |remote| | |
begin | |
return fetch_remote_hash(spec, remote) | |
rescue | |
end | |
end | |
nil | |
end | |
def fetch_remote_hash(spec, remote) | |
hash = nil | |
if URI(remote).host == 'rubygems.org' | |
uri = "#{remote}/api/v1/versions/#{spec.name}.json" | |
puts "Getting SHA256 from: #{uri}" if $VERBOSE | |
open uri do |io| | |
versions = JSON.parse(io.read) | |
if found_version = versions.find{|obj| obj['number'] == spec.version.to_s } | |
hash = found_version['sha'] | |
break | |
end | |
end | |
end | |
uri = "#{remote}/gems/#{spec.name}-#{spec.version}.gem" | |
if hash | |
begin | |
nix_prefetch_url(uri, hash)[SHA256_16] | |
rescue | |
nix_prefetch_url(uri)[SHA256_32] | |
end | |
else | |
nix_prefetch_url(uri)[SHA256_32] | |
end | |
end | |
def convert_rubygems | |
remotes = spec.source.remotes.map{|remote| remote.to_s.sub(/\/+$/, '') } | |
hash = fetch_local_hash(spec) || fetch_remotes_hash(spec, remotes) | |
hash = sh(NIX_HASH, '--type', 'sha256', '--to-base32', hash)[SHA256_32] | |
fail "couldn't fetch hash for #{spec.name}-#{spec.version}" unless hash | |
puts "#{hash} => #{spec.name}-#{spec.version}.gem" if $VERBOSE | |
{ | |
type: 'gem', | |
remotes: remotes, | |
sha256: hash | |
} | |
end | |
def convert_git | |
revision = spec.source.options.fetch('revision') | |
uri = spec.source.options.fetch('uri') | |
hash = nix_prefetch_git(uri, revision)[/^\h{64}$/m] | |
hash = sh(NIX_HASH, '--type', 'sha256', '--to-base32', hash)[SHA256_32] | |
fail "couldn't fetch hash for #{spec.name}-#{spec.version}" unless hash | |
puts "#{hash} => #{uri}" if $VERBOSE | |
{ | |
type: 'git', | |
url: uri.to_s, | |
rev: revision, | |
sha256: hash, | |
fetchSubmodules: false | |
} | |
end | |
end | |
end | |
exit unless $PROGRAM_NAME == __FILE__ | |
require 'optparse' | |
options = {} | |
op = OptionParser.new do |o| | |
o.on '--gemset=gemset.nix', 'path to the gemset.nix' do |value| | |
options[:gemset] = File.expand_path(value) | |
end | |
o.on '--lockfile=Gemfile.lock', 'path to the Gemfile.lock' do |value| | |
options[:lockfile] = File.expand_path(value) | |
end | |
o.on '-d', '--dependencies', 'include gem dependencies' do | |
options[:deps] = true | |
end | |
o.on '-q', '--quiet', 'only output errors' do | |
options[:quiet] = true | |
end | |
o.on '-v', '--version', 'show the version of bundix' do | |
puts Bundix::VERSION | |
exit | |
end | |
end | |
op.parse! | |
$VERBOSE = !options[:quiet] | |
gemset = Bundix.new(options).convert | |
tempfile = Tempfile.new('gemset.nix', encoding: 'UTF-8') | |
begin | |
Bundix.object2nix(gemset, 2, tempfile) | |
tempfile.close | |
FileUtils.cp(tempfile.path, options[:gemset]) | |
ensure | |
tempfile.close! | |
tempfile.unlink | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment