├── .gitignore ├── env ├── bin └── bundix ├── lib ├── bundix.rb └── bundix │ ├── bundler_ext.rb │ ├── prefetcher │ ├── cache.rb │ └── wrapper.rb │ ├── manifest.rb │ ├── objects.rb │ ├── prefetcher.rb │ └── cli.rb ├── TODO ├── shell.nix ├── Gemfile.lock ├── Gemfile ├── gemset.nix ├── bundix.gemspec ├── default.nix └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .bundix/ 2 | .bundle/ 3 | result 4 | -------------------------------------------------------------------------------- /env: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NIX_PATH=nixpkgs=$HOME/src/nixpkgs:$NIX_PATH nix-shell --command zsh 4 | -------------------------------------------------------------------------------- /bin/bundix: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundix/cli' 4 | Bundix::CLI.start(ARGV, debug: true) 5 | -------------------------------------------------------------------------------- /lib/bundix.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'pathname' 3 | 4 | require 'bundix/bundler_ext' 5 | require 'bundix/objects' 6 | 7 | # Generates a Nix expression for bundler-managed dependencies. 8 | module Bundix 9 | end 10 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Make sure .bundle/config isn't modified. 2 | * Use extconf.rb to make patching paths easier. 3 | * Bundix cache needs to take into account the flags used to fetch the git repo 4 | (e.g. --leave-dotGit, --fetch-submodules) 5 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with (import {}); 2 | 3 | runCommand "dummy" { 4 | buildInputs = [ ruby_2_1_3 bundler_HEAD ]; 5 | shellHook = '' 6 | export GEM_HOME=$HOME/.gem/ruby/2.1.3 7 | mkdir -p $GEM_HOME 8 | ''; 9 | } '' 10 | 11 | '' 12 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | bundix (0.1.0) 5 | bundler (~> 1.7.9) 6 | thor (~> 0.19.1) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | thor (0.19.1) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | bundix! 18 | -------------------------------------------------------------------------------- /lib/bundix/bundler_ext.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | 3 | module Bundler 4 | extend self 5 | 6 | def setup(*args) 7 | Bundix.setup(*args) 8 | end 9 | 10 | # Disable the install command completely. 11 | class Installer 12 | def run(_) 13 | raise Bundler::InstallError, "Reload your Nix shell to install all gem dependencies" 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # We need bundler 1.8, which hasn't been released yet. 4 | gemspec 5 | 6 | #gem 'bundler', 7 | # :git => 'https://github.com/bundler/bundler.git', 8 | # :ref => 'a2343c9eabf5403d8ffcbca4dea33d18a60fc157' 9 | 10 | # Use my branch until this PR is accepted: 11 | # https://github.com/bundler/bundler/pull/3349 12 | #gem 'bundler', 13 | # :git => 'https://github.com/cstrahan/bundler.git', 14 | # :ref => 'b233205ec4b474e97c8dc40be9c53a41a70df0e3' 15 | -------------------------------------------------------------------------------- /gemset.nix: -------------------------------------------------------------------------------- 1 | { 2 | "bundix" = { 3 | version = "0.1.0"; 4 | source = { 5 | type = "path"; 6 | path = ./.; 7 | pathString = "."; 8 | }; 9 | dependencies = [ 10 | "bundler" 11 | "thor" 12 | ]; 13 | }; 14 | "bundler" = { 15 | version = "1.7.9"; 16 | source = { 17 | type = "gem"; 18 | sha256 = "1gd201rh17xykab9pbqp0dkxfm7b9jri02llyvmrc0c5bz2vhycm"; 19 | }; 20 | }; 21 | "thor" = { 22 | version = "0.19.1"; 23 | source = { 24 | type = "gem"; 25 | sha256 = "08p5gx18yrbnwc6xc0mxvsfaxzgy2y9i78xq7ds0qmdm67q39y4z"; 26 | }; 27 | }; 28 | } -------------------------------------------------------------------------------- /bundix.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'bundix' 3 | s.version = '0.1.0' 4 | s.licenses = ['MIT'] 5 | s.homepage = 'https://github.com/cstrahan/bundix' 6 | s.summary = "Creates Nix packages from Gemfiles." 7 | s.description = "Creates Nix packages from Gemfiles." 8 | s.authors = ["Alexander Flatter" "Charles Strahan"] 9 | s.email = 'rubycoder@example.com' 10 | s.files = Dir["bin/*"] + Dir["lib/**/*.rb"] 11 | s.bindir = "bin" 12 | s.executables = [ "bundix" ] 13 | s.add_runtime_dependency 'bundler', '~> 1.7.9' 14 | s.add_runtime_dependency 'thor', '~> 0.19.1' 15 | end 16 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | lib = pkgs.lib; 4 | ruby = pkgs.ruby_2_1_3.override { cursesSupport = true; }; 5 | loadRubyEnv = pkgs.loadRubyEnv; 6 | 7 | filePredicate = path: lib.any (suff: lib.hasSuffix suff path) [ 8 | ".rb" 9 | ".gemspec" 10 | "bundix" 11 | ]; 12 | srcPredicate = path: type: 13 | (type == "directory" && !lib.hasSuffix ".git" path) || filePredicate path; 14 | src = builtins.filterSource srcPredicate ./.; 15 | 16 | gemset = loadRubyEnv { 17 | inherit ruby; 18 | gemset = ./gemset.nix; 19 | fixes.bundix = attrs: { 20 | inherit src; 21 | }; 22 | }; 23 | 24 | in 25 | 26 | gemset.bundix 27 | -------------------------------------------------------------------------------- /lib/bundix/prefetcher/cache.rb: -------------------------------------------------------------------------------- 1 | require 'bundix/prefetcher' 2 | require 'yaml' 3 | 4 | class Bundix::Prefetcher::Cache 5 | class << self 6 | def read(pathname) 7 | new(YAML.load(pathname.read)) 8 | end 9 | end 10 | 11 | def initialize(content = {}) 12 | @cache = content 13 | end 14 | 15 | def has?(source) 16 | !!get(source) 17 | end 18 | 19 | def get(source) 20 | source.components.inject(@cache) do |hash, component| 21 | hash[component] || break 22 | end 23 | end 24 | 25 | def set(source) 26 | return unless source.sha256 27 | 28 | components = source.components.dup 29 | last = components.pop 30 | 31 | hash = components.inject(@cache) do |acc, component| 32 | acc[component] ||= Hash.new 33 | end 34 | 35 | hash[last] = source.sha256 36 | end 37 | 38 | # @param [Pathname] path 39 | def write(path) 40 | path.open('w') { |file| file.write(YAML.dump(@cache)) } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/bundix/prefetcher/wrapper.rb: -------------------------------------------------------------------------------- 1 | require 'bundix/prefetcher' 2 | 3 | # Wraps `nix-prefetch-scripts` to provide consistent output. 4 | module Bundix::Prefetcher::Wrapper 5 | extend self 6 | 7 | def git(repo, rev, submodules) 8 | # nix-prefetch-git returns a full sha25 hash 9 | base16 = exec("HOME=/homeless-shelter nix-prefetch-git #{repo} #{rev} --hash sha256 --leave-dotGit #{"--fetch-submodules" if submodules}") 10 | assert_length!(base16, 64) 11 | assert_format!(base16, /^[a-f0-9]+$/) 12 | 13 | # base32-encode for consistency 14 | base32 = exec("nix-hash --type sha256 --to-base32 #{base16}") 15 | assert_length!(base32, 52) 16 | assert_format!(base32, /^[a-z0-9]+$/) 17 | 18 | base32 19 | end 20 | 21 | def url(url) 22 | hash = exec("nix-prefetch-url #{url}") 23 | 24 | # nix-prefetch-url returns a base32-encoded sha256 hash 25 | assert_length!(hash, 52) 26 | assert_format!(hash, /^[a-z0-9]+$/) 27 | 28 | hash 29 | end 30 | 31 | def assert_length!(string, expected_length) 32 | unless string.length == expected_length 33 | raise "Invalid checksum length; expected #{length}, got #{string.length}" 34 | end 35 | end 36 | 37 | def assert_format!(string, regexp) 38 | unless string =~ regexp 39 | raise "Invalid checksum format: #{string}" 40 | end 41 | end 42 | 43 | def exec(command) 44 | output = `#{command}` 45 | raise Thor::Error.new("Prefetch failed: #{command}") unless $?.success? 46 | output.strip.split("\n").last 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/bundix/manifest.rb: -------------------------------------------------------------------------------- 1 | require 'bundix' 2 | require 'pathname' 3 | 4 | class Bundix::Manifest 5 | attr_reader :gems 6 | 7 | def initialize(gems, lockfile, target) 8 | @lockfile = Pathname.new(lockfile).expand_path 9 | @target = Pathname.new(target).expand_path 10 | @gems = gems.sort_by { |g| g.name } 11 | end 12 | 13 | def relative_path(path) 14 | path = Pathname.new(path) 15 | path = @lockfile.dirname + path if path.relative? 16 | path = path.relative_path_from(@target.dirname) 17 | 18 | "./#{path.to_s}" 19 | end 20 | 21 | def to_nix 22 | template = File.read(__FILE__).split('__END__').last.strip 23 | ERB.new(template, nil, '->').result(binding) 24 | end 25 | end 26 | 27 | __END__ 28 | { 29 | <%- gems.each do |gem| -%> 30 | <%= gem.name.inspect %> = { 31 | version = "<%= gem.version %>"; 32 | source = { 33 | type = "<%= gem.source.type %>"; 34 | <%- if gem.source.type == 'git' -%> 35 | url = "<%= gem.source.url %>"; 36 | rev = "<%= gem.source.revision %>"; 37 | sha256 = "<%= gem.source.sha256 %>"; 38 | fetchSubmodules = <%= gem.source.submodules %>; 39 | <%- elsif gem.source.type == 'gem' -%> 40 | sha256 = "<%= gem.source.sha256 %>"; 41 | <%- elsif gem.source.type == 'path' -%> 42 | path = <%= relative_path(gem.source.path) %>; 43 | pathString = <%= gem.source.path.inspect %>; 44 | <%- end -%> 45 | }; 46 | <%- if gem.dependencies.any? -%> 47 | dependencies = [ 48 | <%= gem.dependencies.sort.map {|d| d.inspect}.join("\n ") %> 49 | ]; 50 | <%- end -%> 51 | }; 52 | <%- end -%> 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bundix makes it easy to package your Bundler-enabled applications with the Nix 2 | package manager. 3 | 4 | ### Is it "Production Ready™"? 5 | 6 | ![DANGER: EXPERIMENTAL](https://raw.github.com/cryptosphere/cryptosphere/master/images/experimental.png) 7 | 8 | Nope. It's work-in-progress. 9 | 10 | ### Installation 11 | 12 | Just clone the repo, Nix can handle everything else. 13 | 14 | ``` 15 | git clone https://github.com/aflatter/bundix 16 | ``` 17 | 18 | ### How does it work? 19 | 20 | Bundix builds a definition of your Ruby environment using Bundler and writes a 21 | Nix expression for it. The generated expression can then be loaded and all the 22 | Gem handling etc. will now be handled by Nix. 23 | 24 | ### Usage 25 | 26 | 1. Change to your project's directory. 27 | 2. Generate your definition: 28 | `nix-shell /path/to/bundix/repo --shell 'bundix expr'` 29 | 3. Load the definition using `nixpkgs.loadRubyEnv ./.bundix/definition.nix {}`. 30 | 31 | Example: 32 | 33 | ``` 34 | let 35 | pkgs = import {}; 36 | stdenv = pkgs.stdenv; 37 | ruby = pkgs.ruby21; 38 | rubyLibs = pkgs.ruby21Libs; 39 | buildRubyGem = rubyLibs.buildRubyGem; 40 | 41 | rubyEnv = pkgs.loadRubyEnv ./.bundix/definition.nix { 42 | inherit ruby; 43 | }; 44 | 45 | in with pkgs; rec { 46 | inherit rubyEnv; 47 | 48 | test = stdenv.mkDerivation rec { 49 | name = "test"; 50 | builder = ./builder.sh; 51 | buildInputs = [ rubyEnv.ruby ]; 52 | 53 | src = ./.; 54 | 55 | shellHook = '' 56 | export GEM_PATH=${lib.concatStringsSep ":" rubyEnv.gemPath} 57 | ''; 58 | }; 59 | } 60 | ``` 61 | 62 | 63 | ### Known issues 64 | 65 | - Git repositories that host multiple gems are not supported yet. A single gem 66 | per repository will work fine. 67 | - `path` sources are not supported. 68 | - The ruby version specified by your Gemfile is read but not used yet. 69 | Pass `{ ruby = yourRuby; }` to `loadRubyEnv` instead. 70 | - `Bundler.setup` and friends still have to be stubbed out to do nothing. 71 | - There's no support for gem groups yet. All gems are installed. 72 | -------------------------------------------------------------------------------- /lib/bundix/objects.rb: -------------------------------------------------------------------------------- 1 | require 'bundix' 2 | 3 | module Bundix 4 | # The {Source} class represents all information necessary to fetch the source 5 | # of one or more gems as required by a Nix derivation. 6 | module Source 7 | class Base 8 | attr_writer :sha256 9 | 10 | def initialize 11 | raise NotImplementedError 12 | end 13 | 14 | def sha256 15 | @sha256 ||= nil 16 | end 17 | 18 | def type 19 | self.class.name.split('::').last.downcase 20 | end 21 | 22 | def components 23 | [type] 24 | end 25 | 26 | def hash 27 | components.hash 28 | end 29 | end 30 | 31 | class Git < Base 32 | attr_reader :url 33 | attr_reader :revision 34 | attr_reader :submodules 35 | 36 | def initialize(url, revision, submodules, sha256 = nil) 37 | @url = url 38 | @revision = revision 39 | @submodules = submodules 40 | @sha256 = sha256 41 | end 42 | 43 | def components 44 | super + [{ 45 | "url" => url, 46 | "revision" => revision, 47 | "submodules" => submodules 48 | }] 49 | end 50 | end 51 | 52 | class Gem < Base 53 | attr_reader :url 54 | 55 | def initialize(url, sha256 = nil) 56 | @url = url 57 | @sha256 = sha256 58 | end 59 | 60 | def components 61 | super + [url] 62 | end 63 | end 64 | 65 | class Path < Base 66 | attr_reader :path 67 | attr_reader :original_path 68 | 69 | def initialize(path) 70 | @path = path 71 | end 72 | end 73 | end 74 | 75 | class Dependency 76 | def initialize(dependency) 77 | @dependency = dependency 78 | end 79 | 80 | def name 81 | @dependency.name 82 | end 83 | end 84 | 85 | class Gem 86 | attr_reader :source 87 | attr_reader :dependencies 88 | 89 | def initialize(spec, source, dependencies) 90 | @spec = spec 91 | @source = source 92 | @dependencies = dependencies 93 | end 94 | 95 | def name 96 | @spec.name 97 | end 98 | 99 | def drv_name 100 | "#{@spec.name}-#{version}" 101 | end 102 | 103 | def version 104 | @spec.version.to_s 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/bundix/prefetcher.rb: -------------------------------------------------------------------------------- 1 | require 'bundix' 2 | 3 | class Bundix::Prefetcher 4 | require 'bundix/prefetcher/cache' 5 | require 'bundix/prefetcher/wrapper' 6 | 7 | attr_reader :wrapper 8 | attr_reader :shell 9 | 10 | def initialize(shell, wrapper = Wrapper) 11 | @shell = shell 12 | @wrapper = Wrapper 13 | end 14 | 15 | # @param [Bundler::SpecSet] specs 16 | # @param [Pathname] cache_path 17 | # @return Array 18 | def run(specs, cache_path) 19 | # Bundler flattens all of the dependencies that we care about. 20 | dep_names = Set.new(specs.map {|s| s.name}) 21 | 22 | cache = load_cache(cache_path) 23 | 24 | result = specs.map do |spec| 25 | deps = spec.dependencies.map {|dep| dep.name}.select {|dep| dep_names.include?(dep)}.sort 26 | source = build_source(spec) 27 | source.sha256 = if cache.has?(source) 28 | shell.say_status('Cached', spec) 29 | cache.get(source) 30 | else 31 | shell.say_status('Prefetching', spec) 32 | prefetch(source) 33 | end 34 | 35 | Bundix::Gem.new(spec, source, deps) 36 | end 37 | 38 | write_cache(cache_path, result) 39 | 40 | result 41 | end 42 | 43 | def build_source(spec) 44 | source = spec.source 45 | case source 46 | when Bundler::Source::Rubygems 47 | url = File.join(source.remotes.first.to_s, 'downloads', "#{spec.name}-#{spec.version}.gem") 48 | Bundix::Source::Gem.new(url) 49 | when Bundler::Source::Git 50 | Bundix::Source::Git.new(source.uri, source.revision, !!source.submodules) 51 | when Bundler::Source::Path 52 | Bundix::Source::Path.new(source.path.to_s) 53 | else 54 | fail "Unhandled source type: #{source.class}" 55 | end 56 | end 57 | 58 | # @param [Pathname] cache_path 59 | # @return [Cache] 60 | def load_cache(cache_path) 61 | cache_path.exist? ? Cache.read(cache_path) : Cache.new 62 | end 63 | 64 | # @param [Bundler::LazySpecification] spec 65 | def prefetch(source) 66 | case source 67 | when Bundix::Source::Gem 68 | wrapper.url(source.url) 69 | when Bundix::Source::Git 70 | wrapper.git(source.url, source.revision, source.submodules) 71 | end 72 | end 73 | 74 | # @param [Pathname] cache_path 75 | # @param [Array] gems 76 | def write_cache(cache_path, gems) 77 | cache_dir = cache_path.dirname 78 | cache_dir.mkpath unless cache_dir.exist? 79 | 80 | cache = Cache.new 81 | gems.map(&:source).each { |source| cache.set(source) } 82 | cache.write(cache_path) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/bundix/cli.rb: -------------------------------------------------------------------------------- 1 | require 'thor' 2 | require 'bundix' 3 | require 'fileutils' 4 | require 'pathname' 5 | 6 | # Because bundler gives me no choice... 7 | Bundler.module_eval do 8 | class << self 9 | def requires_sudo? 10 | true 11 | end 12 | end 13 | end 14 | 15 | Bundler::Source::Git.class_eval do 16 | def allow_git_ops? 17 | # was: @allow_remote || @allow_cached 18 | true 19 | end 20 | 21 | # Prevent bundler from trying to screw with /nix/store 22 | def install_path 23 | @install_path ||= begin 24 | git_scope = "#{base_name}-#{shortref_for_path(revision)}" 25 | Bundler.user_bundle_path.join(Bundler.ruby_scope).join(git_scope) 26 | end 27 | end 28 | end 29 | 30 | Bundler::Settings.class_eval do 31 | # don't pollute $PWD with .bundle/config 32 | def set_key(key, value, hash, file) 33 | key = key_for(key) 34 | 35 | unless hash[key] == value 36 | hash[key] = value 37 | hash.delete(key) if value.nil? 38 | end 39 | 40 | value 41 | end 42 | end 43 | 44 | class Bundix::CLI < Thor 45 | include Thor::Actions 46 | default_task :expr 47 | 48 | class Prefetch < Thor 49 | desc 'git URL REVISION', 'Prefetches a git repository' 50 | def git(url, revision) 51 | puts Prefetch::Wrapper.git(url, revision) 52 | end 53 | 54 | desc 'url URL', 'Prefetches a file from a URL' 55 | def url(url) 56 | puts Prefetcher::Wrapper.url(url) 57 | end 58 | end 59 | 60 | desc "init", "Sets up your project for use with Bundix" 61 | def init 62 | raise NotImplemented 63 | end 64 | 65 | desc 'expr', 'Creates a Nix expression for your project' 66 | option :gemfile, type: :string, default: 'Gemfile', 67 | desc: "Path to the project's Gemfile" 68 | option :lockfile, type: :string, default: 'Gemfile.lock', 69 | desc: "Path to the project's Gemfile.lock" 70 | option :cachefile, type: :string, default: "#{ENV['HOME']}/.bundix/cache" 71 | option :target, type: :string, default: 'gemset.nix', 72 | desc: 'Path to the target file' 73 | option :lock, type: :boolean, 74 | desc: 'Should the lockfile be created/updated?' 75 | def expr 76 | require 'bundix/prefetcher' 77 | require 'bundix/manifest' 78 | 79 | Bundler.settings[:no_install] = true 80 | 81 | lockfile = Pathname.new(options[:lockfile]).expand_path 82 | specs = nil 83 | 84 | if options[:lock] 85 | say("Generating lockfile...", :green) 86 | gemfile = Pathname.new(options[:gemfile]) 87 | definition = nil 88 | Dir.chdir(gemfile.dirname) do 89 | definition = Bundler::Definition.build(gemfile.basename, lockfile, {}) 90 | definition.resolve_remotely! 91 | specs = definition.resolve 92 | end 93 | create_file(lockfile, definition.to_lock, force: true) 94 | else 95 | lockfile = Bundler::LockfileParser.new(Bundler.read_file(options[:lockfile])) 96 | specs = lockfile.specs 97 | end 98 | 99 | say("Pre-fetching gems...", :green) 100 | gems = Bundix::Prefetcher.new(shell).run(specs, Pathname.new(options[:cachefile])) 101 | 102 | say("Generating gemset...", :green) 103 | manifest = Bundix::Manifest.new(gems, options[:lockfile], options[:target]) 104 | create_file(@options[:target], manifest.to_nix, force: true) 105 | end 106 | 107 | desc 'prefetch', 'Conveniently wraps nix-prefetch-scripts' 108 | subcommand :prefetch, Prefetch 109 | end 110 | --------------------------------------------------------------------------------