summaryrefslogtreecommitdiff
path: root/scripts/import-rubygem
blob: 6b869a7d903134f23ab89cff31324f91d32510a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env ruby
#
# Create a stratum to integrate a Ruby project in Baserock, using RubyGems
#
# Copyright (C) 2014  Codethink Limited
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

require 'bundler'
require 'optparse'
require 'yaml'

BASEROCK_RUBY_VERSION = '2.0.0'

def parse_options(arguments)
    # No options so far ..
    opts = OptionParser.new

    opts.banner = "Usage: import-ruby PROJECT_DIR OUTPUT_DIR"
    opts.separator ""
    opts.separator "This tool reads the Gemfile and optionally the " +
                   "Gemfile.lock from a Ruby project "
    opts.separator "source tree in PROJECT_DIR. It outputs a stratum " +
                   "morphology and a set of chunk "
    opts.separator "morphology files to OUTPUT_DIR."

    parsed_arguments = opts.parse!(arguments)

    if parsed_arguments.length != 2 then
        STDERR.puts opts.help
        exit 1
    end

    parsed_arguments
end

def get_project_name(project_dir_name)
    # One Git repo can produce any number of Gems, or none, so it's hard to
    # work out a project name that way. Instead, use the repo name :)
    project_name = File.basename(project_dir_name)
end

def load_gemfile()
    # Load and parse the Gemfile and, if found, the Gemfile.lock file.
    definition = Bundler::Definition.build(
        'Gemfile', 'Gemfile.lock', update=false)
end

def get_all_specs_for_project(dir_name)
    Dir.chdir(dir_name) { begin
        load_gemfile.specs
    rescue Bundler::GemNotFound
        # If we're missing some Gem info, try remotely resolving. This is very
        # slow so it's nice if it can be avoided. Perhaps setting up a local
        # mirror of the necessary specs would avoid this problem. There seems
        # to be no way to "reset" the Definition instance after the exception,
        # so we have to call load_gemfile again.
        STDERR.puts "Resolving definitions remotely (this may take a while!)"
        load_gemfile.resolve_remotely!
    rescue Bundler::GemfileNotFound
        STDERR.puts "Did not find a Gemfile in #{dir_name}."
        exit
    end }
end

def generate_morphs_for_specset(project_name, specs)
    # Chunk names are the Gem's "full name" (name + version number), so that we
    # don't break in the rare but possible case that two different versions of
    # the same Gem are required for something to work. It'd be nicer to only
    # use the full_name if we detect such a conflict. If we do, at least this
    # function below can be removed with the much simpler:
    #   spec.deps.collect |dep| dep.name
    runtime_depends = proc do |spec|
        result = []
        spec.dependencies.each do |dep|
            next if dep.type == :development
            found = specs[dep]
            if found.length != 1
                raise Exception,
                    "Unsure which Gem to use for #{dep}, got #{found}"
            end
            result << found[0].full_name
        end
        result
    end

    description = 'Automatically generated by import-ruby. This is a ' +
                  'prototype of a method for integrating RubyGems into ' +
                  'Baserock.'

    bin_dir = "\"$DESTDIR/$PREFIX/bin\""
    gem_dir = "\"$DESTDIR/$PREFIX/lib/ruby/gems/#{BASEROCK_RUBY_VERSION}\""

    chunk_morphs = specs.collect do |spec|
        # There's more splitting to be done, but putting the docs in the
        # correct artifact is the single biggest win for enabling smaller
        # system images.
        split_rules = [
            {
                'artifact' => "#{spec.full_name}-doc",
                'include' => [
                    "usr/lib/ruby/gems/#{BASEROCK_RUBY_VERSION}/doc/.*"
                ]
            }
        ]

        install_commands = [
            "mkdir -p #{gem_dir}",
            "gem install --install-dir #{gem_dir} --bindir #{bin_dir} " +
                 "--ignore-dependencies --local #{spec.full_name}.gem"
        ]

        {
            'name' => spec.full_name,
            'kind' => 'chunk',
            'description' => description,
            'build-system' => 'manual',
            # FIXME: this is not how we should calculate the URL field!
            'gem-url' => "http://rubygems.org/downloads/#{spec.full_name}.gem",
            'products' => split_rules,
            'install-commands' => install_commands
        }
    end

    chunks = specs.collect do |spec|
        {
            'name' => spec.full_name,
            'description' => description,
            # This is a dummy value; there is no repo for these chunks.
            # The 'repo' field should perhaps become optional!
            'repo' => 'baserock:baserock/definitions',
            'ref' => 'master',
            'morph' => File.join(project_name, spec.full_name + '.morph'),
            # Runtime depends must be present at "build" (Gem install) time.
            'build-depends' => runtime_depends.call(spec),
            # This feature is not in morph.git master yet
            'build-mode' => 'rubygem',
        }
    end

    stratum_morph = {
        'name' => project_name,
        'kind' => 'stratum',
        'description' => description,
        'build-depends' => [
            { 'morph' => 'ruby' }
        ],
        'chunks' => chunks,
    }

    return [stratum_morph] + chunk_morphs
end

def write_morphs(morphs, project_name, target_dir_name)
    target_dir_name = File.join(target_dir_name, project_name)
    FileUtils.makedirs(target_dir_name)
    Dir.chdir(target_dir_name) do
        morphs.each do |morph|
            morph_filename = morph['name'] + '.morph'
            File.open(morph_filename, 'w') do |file|
                file.write(YAML.dump(morph))
            end
        end
    end
end

def run
    project_dir_name, target_dir_name = parse_options(ARGV)

    project_name = get_project_name(project_dir_name)
    specset = get_all_specs_for_project(project_dir_name)

    morphs = generate_morphs_for_specset(project_name, specset)

    write_morphs(morphs, project_name, target_dir_name)
end

run