summaryrefslogtreecommitdiff
path: root/baserockimport/exts/rubygems.to_chunk
blob: 1573d8bcea959917c7970ed1afbdc45ae47b186d (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
#!/usr/bin/env ruby
#
# Create a chunk morphology to build a RubyGem in Baserock
#
# 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_relative 'importer_base'
require_relative 'importer_bundler_extensions'

BANNER = "Usage: rubygems.to_chunk SOURCE_DIR GEM_NAME [VERSION]"

DESCRIPTION = <<-END
This tool looks in SOURCE_DIR to generate a chunk morphology with build
instructions for GEM_NAME. If VERSION is supplied, it is used to check that the
build instructions will produce the expected version of the Gem.

It is intended for use with the `baserock-import` tool.
END

class RubyGemChunkMorphologyGenerator < Importer::Base
  include Importer::BundlerExtensions

  def parse_options(arguments)
    opts = create_option_parser(BANNER, DESCRIPTION)

    parsed_arguments = opts.parse!(arguments)

    if parsed_arguments.length != 2 && parsed_arguments.length != 3
      STDERR.puts "Expected 2 or 3 arguments, got #{parsed_arguments}."
      opts.parse(['-?'])
      exit 255
    end

    source_dir, gem_name, expected_version = parsed_arguments
    source_dir = File.absolute_path(source_dir)
    if expected_version != nil
      expected_version = Gem::Version.new(expected_version.dup)
    end
    [source_dir, gem_name, expected_version]
  end

  def chunk_name_for_gemspec(spec)
    # 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.
    spec.full_name
  end

  def is_signed_gem(spec)
    spec.signing_key != nil
  end

  def generate_chunk_morph_for_gem(spec)
    description = 'Automatically generated by rubygems.to_chunk'

    bin_dir = "\"$DESTDIR/$PREFIX/bin\""
    gem_dir = "\"$DESTDIR/$(gem environment home)\""

    # 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.
    #
    # Adding this to Morph's default ruleset is painful, because:
    #   - Changing the default split rules triggers a rebuild of everything.
    #   - The whole split rule code needs reworking to prevent overlaps and to
    #     make it possible to extend rules without creating overlaps. It's
    #     otherwise impossible to reason about.

    split_rules = [
      {
        'artifact' => "#{spec.full_name}-doc",
        'include' => [
          'usr/lib/ruby/gems/\d[\w.]*/doc/.*'
        ]
      }
    ]

    # It'd be rather tricky to include these build instructions as a
    # BuildSystem implementation in Morph. The problem is that there's no
    # way for the default commands to know what .gemspec file they should
    # be building. It doesn't help that the .gemspec may be in a subdirectory
    # (as in Rails, for example).
    #
    # Note that `gem help build` says the following:
    #
    #   The best way to build a gem is to use a Rakefile and the
    #   Gem::PackageTask which ships with RubyGems.
    #
    # It's often possible to run `rake gem`, but this may require Hoe,
    # rake-compiler, Jeweler or other assistance tools to be present at Gem
    # construction time. It seems that many Ruby projects that use these tools
    # also maintain an up-to-date generated .gemspec file, which means that we
    # can get away with using `gem build` just fine in many cases.
    #
    # Were we to use `setup.rb install` or `rake install`, programs that loaded
    # with the 'rubygems' library would complain that required Gems were not
    # installed. We must have the Gem metadata available, and `gem build; gem
    # install` seems the easiest way to achieve that.

    configure_commands = []

    if is_signed_gem(spec)
      # This is a best-guess hack for allowing unsigned builds of Gems that are
      # normally built signed. There's no value in building signed Gems when we
      # control the build and deployment environment, and we obviously can't
      # provide the private key of the Gem's maintainer.
      configure_commands <<
        "sed -e '/cert_chain\\s*=/d' -e '/signing_key\\s*=/d' -i " \
        "#{spec.name}.gemspec"
    end

    build_commands = [
      "gem build #{spec.name}.gemspec",
    ]

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

    {
      'name' => chunk_name_for_gemspec(spec),
      'kind' => 'chunk',
      'description' => description,
      'build-system' => 'manual',
      'products' => split_rules,
      'configure-commands' => configure_commands,
      'build-commands' => build_commands,
      'install-commands' => install_commands,
    }
  end

  def run
    source_dir_name, gem_name, expected_version = parse_options(ARGV)

    log.info("Creating chunk morph for #{gem_name} based on " \
             "source code in #{source_dir_name}")

    resolved_specs = Dir.chdir(source_dir_name) do
      # FIXME: resolving the specs for all the dependencies of the target gem
      # isn't necessary here. In fact, just reading/executing the .gemspec
      # would be enough, and would speed this program up and remove a lot of
      # pointless network access to rubygems.org.
      definition = create_bundler_definition_for_gemspec(gem_name, source_dir_name)
      definition.resolve_remotely!
    end

    spec = get_spec_for_gem(resolved_specs, gem_name)
    validate_spec(spec, source_dir_name, expected_version)

    morph = generate_chunk_morph_for_gem(spec)
    write_morph(STDOUT, morph)
  end
end

RubyGemChunkMorphologyGenerator.new.run