fpm-fry 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/bin/fpm-fry +10 -0
  3. data/lib/cabin/nice_output.rb +70 -0
  4. data/lib/fpm/fry/block_enumerator.rb +25 -0
  5. data/lib/fpm/fry/build_output_parser.rb +22 -0
  6. data/lib/fpm/fry/client.rb +162 -0
  7. data/lib/fpm/fry/command/cook.rb +370 -0
  8. data/lib/fpm/fry/command.rb +90 -0
  9. data/lib/fpm/fry/detector.rb +109 -0
  10. data/lib/fpm/fry/docker_file.rb +149 -0
  11. data/lib/fpm/fry/joined_io.rb +63 -0
  12. data/lib/fpm/fry/os_db.rb +35 -0
  13. data/lib/fpm/fry/plugin/alternatives.rb +90 -0
  14. data/lib/fpm/fry/plugin/edit_staging.rb +66 -0
  15. data/lib/fpm/fry/plugin/exclude.rb +18 -0
  16. data/lib/fpm/fry/plugin/init.rb +53 -0
  17. data/lib/fpm/fry/plugin/platforms.rb +10 -0
  18. data/lib/fpm/fry/plugin/script_helper.rb +176 -0
  19. data/lib/fpm/fry/plugin/service.rb +100 -0
  20. data/lib/fpm/fry/plugin.rb +3 -0
  21. data/lib/fpm/fry/recipe/builder.rb +267 -0
  22. data/lib/fpm/fry/recipe.rb +141 -0
  23. data/lib/fpm/fry/source/dir.rb +56 -0
  24. data/lib/fpm/fry/source/git.rb +90 -0
  25. data/lib/fpm/fry/source/package.rb +202 -0
  26. data/lib/fpm/fry/source/patched.rb +118 -0
  27. data/lib/fpm/fry/source.rb +47 -0
  28. data/lib/fpm/fry/stream_parser.rb +98 -0
  29. data/lib/fpm/fry/tar.rb +71 -0
  30. data/lib/fpm/fry/templates/debian/after_install.erb +9 -0
  31. data/lib/fpm/fry/templates/debian/before_install.erb +13 -0
  32. data/lib/fpm/fry/templates/debian/before_remove.erb +13 -0
  33. data/lib/fpm/fry/templates/redhat/after_install.erb +2 -0
  34. data/lib/fpm/fry/templates/redhat/before_install.erb +6 -0
  35. data/lib/fpm/fry/templates/redhat/before_remove.erb +6 -0
  36. data/lib/fpm/fry/templates/sysv.erb +125 -0
  37. data/lib/fpm/fry/templates/upstart.erb +15 -0
  38. data/lib/fpm/fry/ui.rb +12 -0
  39. data/lib/fpm/package/docker.rb +186 -0
  40. metadata +111 -0
@@ -0,0 +1,176 @@
1
+ require 'fpm/fry/plugin'
2
+ module FPM::Fry::Plugin::ScriptHelper
3
+
4
+ class Script
5
+
6
+ attr :renderer
7
+
8
+ def initialize(renderer)
9
+ @renderer = renderer
10
+ end
11
+
12
+ def to_s
13
+ renderer.call(self)
14
+ end
15
+
16
+ def name
17
+ self.class.name.split('::').last.gsub(/([^A-Z])([A-Z])/,'\1_\2').downcase
18
+ end
19
+ end
20
+
21
+ module RenderErb
22
+ def render_path(script, path)
23
+ _erbout = ""
24
+ erb = ERB.new(
25
+ IO.read(File.join(File.dirname(__FILE__),'..','templates',path, "#{script.name}.erb"))
26
+ )
27
+ script.instance_eval(erb.src)
28
+ return _erbout
29
+ end
30
+ end
31
+
32
+ module DebianRenderer
33
+ extend RenderErb
34
+ def self.call(script)
35
+ render_path(script,'debian')
36
+ end
37
+ end
38
+
39
+ module RedhatRenderer
40
+ extend RenderErb
41
+ def self.call(script)
42
+ render_path(script,'redhat')
43
+ end
44
+ end
45
+
46
+ class BeforeInstall < Script
47
+
48
+ # deb: $1 == install
49
+ # rpm: $1 == 1
50
+ attr :install
51
+
52
+ # deb: $1 == upgrade
53
+ # rpm: $1 >= 2
54
+ attr :upgrade
55
+
56
+ def initialize(*_)
57
+ super
58
+ @install = []
59
+ @upgrade = []
60
+ end
61
+
62
+ end
63
+
64
+ class AfterInstall < Script
65
+
66
+ def initialize(*_)
67
+ super
68
+ @configure = []
69
+ end
70
+
71
+ # deb: $1 == configure
72
+ # rpm: -always-
73
+ attr :configure
74
+
75
+ end
76
+
77
+ class BeforeRemove < Script
78
+
79
+ def initialize(*_)
80
+ super
81
+ @remove = []
82
+ @upgrade = []
83
+ end
84
+
85
+ # deb: $1 == remove
86
+ # rpm: $1 == 0
87
+ attr :remove
88
+
89
+ # deb: $1 == upgrade
90
+ # rpm: $1 >= 1
91
+ attr :upgrade
92
+
93
+ end
94
+
95
+ class AfterRemove < Script
96
+
97
+ def initialize
98
+ @remove = []
99
+ @upgrade = []
100
+ end
101
+
102
+ # deb: $1 == upgrade
103
+ # rpm: $1 == 1
104
+ attr :upgrade
105
+
106
+ # deb: $1 == remove
107
+ # rpm: $1 == 0
108
+ attr :remove
109
+
110
+ end
111
+
112
+ NAME_TO_SCRIPT = {
113
+ before_install: BeforeInstall,
114
+ after_install: AfterInstall,
115
+ before_remove: BeforeRemove,
116
+ after_remove: AfterRemove
117
+ }
118
+
119
+ SCRIPT_TO_NAME = NAME_TO_SCRIPT.invert
120
+
121
+ class DSL < Struct.new(:builder)
122
+
123
+ # before(install) => before_install:install
124
+ # before(upgrade) => before_install:upgrade
125
+ # after(install_or_upgrade) => after_install:configure
126
+ # before(remove_for_upgrade) => before_remove:upgrade
127
+ # before(remove) => before_remove:remove
128
+ # after(remove) => after_remove:remove
129
+ # after(remove_for_upgrade) => after_remove:upgrade
130
+
131
+ def after_install_or_upgrade(*scripts)
132
+ find(:after_install).configure.push(*scripts)
133
+ end
134
+
135
+ def before_remove_entirely(*scripts)
136
+ find(:before_remove).remove.push(*scripts)
137
+ end
138
+
139
+ def after_remove_entirely(*scripts)
140
+ find(:after_remove).remove.push(*scripts)
141
+ end
142
+ private
143
+
144
+ def find(type)
145
+ klass = NAME_TO_SCRIPT[type]
146
+ script = builder.script(type).find{|s| s.kind_of? klass }
147
+ if script.nil?
148
+ script = klass.new( renderer )
149
+ builder.script(type,script)
150
+ end
151
+ return script
152
+ end
153
+
154
+ def renderer
155
+ @renderer ||= case(builder.flavour)
156
+ when 'debian' then DebianRenderer
157
+ when 'redhat' then RedhatRenderer
158
+ else
159
+ raise "Unknown flavour: #{builder.flavour.inspect}"
160
+ end
161
+ end
162
+
163
+ end
164
+
165
+ def self.apply(builder, options = {}, &block)
166
+ dsl = DSL.new(builder)
167
+ if block
168
+ if block.arity == 1
169
+ yield dsl
170
+ else
171
+ dsl.instance_eval(&block)
172
+ end
173
+ end
174
+ end
175
+
176
+ end
@@ -0,0 +1,100 @@
1
+ require 'fpm/fry/plugin'
2
+ require 'fpm/fry/plugin/init'
3
+ require 'fpm/fry/plugin/edit_staging'
4
+ require 'erb'
5
+ require 'shellwords'
6
+ module FPM::Fry::Plugin ; module Service
7
+
8
+ class Environment < Struct.new(:name,:command, :description)
9
+
10
+ def render(file)
11
+ _erbout = ""
12
+ erb = ERB.new(
13
+ IO.read(File.join(File.dirname(__FILE__),'..','templates',file))
14
+ )
15
+ eval(erb.src)
16
+ return _erbout
17
+ end
18
+
19
+ end
20
+
21
+ class DSL
22
+
23
+ def initialize(*_)
24
+ super
25
+ @name = nil
26
+ @command = []
27
+ end
28
+
29
+ def name( n = nil )
30
+ if n
31
+ @name = n
32
+ end
33
+ return @name
34
+ end
35
+
36
+ def command( *args )
37
+ if args.any?
38
+ @command = args
39
+ end
40
+ return @command
41
+ end
42
+
43
+ # @api private
44
+ def add!(builder)
45
+ name = self.name || builder.name || raise
46
+ init = Init.detect_init(builder.variables)
47
+ edit = builder.plugin('edit_staging')
48
+ env = Environment.new(name, command, "")
49
+ case(init)
50
+ when 'upstart' then
51
+ edit.add_file "/etc/init/#{name}.conf",StringIO.new( env.render('upstart.erb') )
52
+ edit.ln_s '/lib/init/upstart-job', "/etc/init.d/#{name}"
53
+ builder.plugin('script_helper') do |sh|
54
+ sh.after_install_or_upgrade(<<BASH)
55
+ if status #{Shellwords.shellescape name} 2>/dev/null | grep -q ' start/'; then
56
+ # It has to be stop+start because upstart doesn't pickup changes with restart.
57
+ stop #{Shellwords.shellescape name}
58
+ fi
59
+ start #{Shellwords.shellescape name}
60
+ BASH
61
+ sh.before_remove_entirely(<<BASH)
62
+ if status #{Shellwords.shellescape name} 2>/dev/null | grep -q ' start/'; then
63
+ stop #{Shellwords.shellescape name}
64
+ fi
65
+ BASH
66
+ end
67
+ when 'sysv' then
68
+ edit.add_file "/etc/init.d/#{name}",StringIO.new( env.render('sysv.erb') ), chmod: '750'
69
+ builder.plugin('script_helper') do |sh|
70
+ sh.after_install_or_upgrade(<<BASH)
71
+ update-rc.d #{Shellwords.shellescape name} defaults
72
+ /etc/init.d/#{Shellwords.shellescape name} restart
73
+ BASH
74
+ sh.before_remove_entirely(<<BASH)
75
+ /etc/init.d/#{Shellwords.shellescape name} stop
76
+ update-rc.d -f #{Shellwords.shellescape name} remove
77
+ BASH
78
+ end
79
+ when 'systemd' then
80
+
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ def self.apply(builder, &block)
87
+ d = DSL.new
88
+ if !block
89
+ raise ArgumentError, "service plugin requires a block"
90
+ elsif block.arity == 1
91
+ block.call(d)
92
+ else
93
+ d.instance_eval(&block)
94
+ end
95
+ d.add!(builder)
96
+ return nil
97
+ end
98
+
99
+ end end
100
+
@@ -0,0 +1,3 @@
1
+ module FPM::Fry::Plugin
2
+
3
+ end
@@ -0,0 +1,267 @@
1
+ require 'fpm/fry/recipe'
2
+ require 'forwardable'
3
+ module FPM::Fry
4
+ class Recipe
5
+
6
+ class NotFound < StandardError
7
+ end
8
+
9
+ class PackageBuilder < Struct.new(:variables, :package_recipe)
10
+
11
+ attr :logger
12
+
13
+ def initialize( variables, recipe = PackageRecipe.new, options = {})
14
+ super(variables, recipe)
15
+ @logger = options.fetch(:logger){ Cabin::Channel.get }
16
+ end
17
+
18
+ def flavour
19
+ variables[:flavour]
20
+ end
21
+
22
+ def distribution
23
+ variables[:distribution]
24
+ end
25
+ alias platform distribution
26
+
27
+ def distribution_version
28
+ variables[:distribution_version]
29
+ end
30
+ alias platform_version distribution_version
31
+
32
+ def codename
33
+ variables[:codename]
34
+ end
35
+
36
+ def iteration(value = Not)
37
+ get_or_set('@iteration',value)
38
+ end
39
+ alias revision iteration
40
+
41
+ def version(value = Not)
42
+ get_or_set('@version',value)
43
+ end
44
+
45
+ def name(value = Not)
46
+ get_or_set('@name',value)
47
+ end
48
+
49
+ def vendor(value = Not)
50
+ get_or_set('@vendor',value)
51
+ end
52
+
53
+ def depends( name , options = {} )
54
+ name, options = parse_package(name, options)
55
+ package_recipe.depends[name] = options
56
+ end
57
+
58
+ def conflicts( name , options = {} )
59
+ name, options = parse_package(name, options)
60
+ package_recipe.conflicts[name] = options
61
+ end
62
+
63
+ def provides( name , options = {} )
64
+ name, options = parse_package(name, options)
65
+ package_recipe.provides[name] = options
66
+ end
67
+
68
+ def replaces( name , options = {} )
69
+ name, options = parse_package(name, options)
70
+ package_recipe.replaces[name] = options
71
+ end
72
+
73
+ def files( pattern )
74
+ package_recipe.files << pattern
75
+ end
76
+
77
+ def plugin(name, *args, &block)
78
+ logger.debug('Loading Plugin', name: name, args: args, block: block, load_path: $LOAD_PATH)
79
+ if name =~ /\A\./
80
+ require name
81
+ else
82
+ require File.join('fpm/fry/plugin',name)
83
+ end
84
+ module_name = File.basename(name,'.rb').gsub(/(?:\A|_)([a-z])/){ $1.upcase }
85
+ mod = FPM::Fry::Plugin.const_get(module_name)
86
+ if mod.respond_to? :apply
87
+ mod.apply(self, *args, &block)
88
+ else
89
+ extend(mod)
90
+ end
91
+ end
92
+
93
+ def script(type, value = Not)
94
+ if value != Not
95
+ package_recipe.scripts[type] << value
96
+ end
97
+ return package_recipe.scripts[type]
98
+ end
99
+
100
+ def before_install(*args)
101
+ script(:before_install, *args)
102
+ end
103
+ alias pre_install before_install
104
+ alias preinstall before_install
105
+
106
+ def after_install(*args)
107
+ script(:after_install, *args)
108
+ end
109
+ alias post_install after_install
110
+ alias postinstall after_install
111
+
112
+ def before_remove(*args)
113
+ script(:before_remove, *args)
114
+ end
115
+ alias before_uninstall before_remove
116
+ alias pre_uninstall before_remove
117
+ alias preuninstall before_remove
118
+
119
+ def after_remove(*args)
120
+ script(:after_remove, *args)
121
+ end
122
+ alias after_uninstall after_remove
123
+ alias post_uninstall after_remove
124
+ alias postuninstall after_remove
125
+
126
+ def output_hooks
127
+ package_recipe.output_hooks
128
+ end
129
+
130
+ protected
131
+
132
+ def parse_package( name, options = {} )
133
+ if options.kind_of? String
134
+ options = {version: options}
135
+ end
136
+ case(v = options[:version])
137
+ when String
138
+ if v =~ /\A(<=|<<|>=|>>|<>|=|>|<)(\s*)/
139
+ options[:version] = ' ' + $1 + ' ' + $'
140
+ else
141
+ options[:version] = ' = ' + v
142
+ end
143
+ end
144
+ return name, options
145
+ end
146
+
147
+
148
+ Not = Module.new
149
+ def get_or_set(name, value = Not)
150
+ if value == Not
151
+ return package_recipe.instance_variable_get(name)
152
+ else
153
+ return package_recipe.instance_variable_set(name, value)
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+ class Builder < PackageBuilder
160
+
161
+ attr :recipe
162
+
163
+ def initialize( variables, recipe = Recipe.new, options = {})
164
+ variables = variables.dup
165
+ if variables[:distribution] && !variables[:flavour] && OsDb[variables[:distribution]]
166
+ variables[:flavour] = OsDb[variables[:distribution]][:flavour]
167
+ end
168
+ if !variables[:codename] && OsDb[variables[:distribution]] && variables[:distribution_version]
169
+ codename = OsDb[variables[:distribution]][:codenames].find{|name,version| variables[:distribution_version].start_with? version }
170
+ variables[:codename] = codename[0] if codename
171
+ end
172
+ variables.freeze
173
+ @recipe = recipe
174
+ super(variables, recipe.packages[0], options = {})
175
+ end
176
+
177
+ def load_file( file )
178
+ file = File.expand_path(file)
179
+ begin
180
+ content = IO.read(file)
181
+ rescue Errno::ENOENT => e
182
+ raise NotFound, e
183
+ end
184
+ basedir = File.dirname(file)
185
+ Dir.chdir(basedir) do
186
+ instance_eval(content,file,0)
187
+ end
188
+ end
189
+
190
+ def source( url , options = {} )
191
+ options = options.merge(logger: logger)
192
+ source = Source::Patched.decorate(options) do |options|
193
+ guess_source(url,options).new(url, options)
194
+ end
195
+ recipe.source = source
196
+ end
197
+
198
+ def run(*args)
199
+ if args.first.kind_of? Hash
200
+ options = args.shift
201
+ else
202
+ options = {}
203
+ end
204
+ command = args.shift
205
+ name = options.fetch(:name){ [command,*args].select{|c| c[0] != '-' }.join('-') }
206
+ recipe.steps[name] = Shellwords.join([command, *args])
207
+ end
208
+
209
+ def build_depends( name , options = {} )
210
+ name, options = parse_package(name, options)
211
+ recipe.build_depends[name] = options
212
+ end
213
+
214
+ def input_hooks
215
+ recipe.input_hooks
216
+ end
217
+
218
+ def package(name, &block)
219
+ pr = PackageRecipe.new
220
+ pr.name = name
221
+ pr.version = package_recipe.version
222
+ pr.iteration = package_recipe.iteration
223
+ recipe.packages << pr
224
+ PackageBuilder.new(variables, pr).instance_eval(&block)
225
+ end
226
+
227
+ protected
228
+
229
+ def source_types
230
+ @source_types ||= {
231
+ git: Source::Git,
232
+ http: Source::Package,
233
+ tar: Source::Package,
234
+ dir: Source::Dir
235
+ }
236
+ end
237
+
238
+ def register_source_type( name, klass )
239
+ if !klass.respond_to? :new
240
+ raise ArgumentError.new("Expected something that responds to :new, got #{klass.inspect}")
241
+ end
242
+ source_types[name] = klass
243
+ end
244
+
245
+ NEG_INF = (-1.0/0.0)
246
+
247
+ def guess_source( url, options = {} )
248
+ if w = options[:with]
249
+ return source_types.fetch(w){ raise ArgumentError.new("Unknown source type: #{w}") }
250
+ end
251
+ scores = source_types.values.uniq\
252
+ .select{|klass| klass.respond_to? :guess }\
253
+ .group_by{|klass| klass.guess(url) }\
254
+ .sort_by{|score,_| score.nil? ? NEG_INF : score }
255
+ score, klasses = scores.last
256
+ if score == nil
257
+ raise ArgumentError.new("No source provide found for #{url}.\nMaybe try explicitly setting the type using :with parameter. Valid options are: #{source_types.keys.join(', ')}")
258
+ end
259
+ if klasses.size != 1
260
+ raise ArgumentError.new("Multiple possible source providers found for #{url}: #{klasses.join(', ')}.\nMaybe try explicitly setting the type using :with parameter. Valid options are: #{source_types.keys.join(', ')}")
261
+ end
262
+ return klasses.first
263
+ end
264
+
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,141 @@
1
+ require 'fpm/fry/source'
2
+ require 'fpm/fry/source/package'
3
+ require 'fpm/fry/source/dir'
4
+ require 'fpm/fry/source/patched'
5
+ require 'fpm/fry/source/git'
6
+ require 'fpm/fry/plugin'
7
+ require 'fpm/fry/os_db'
8
+ require 'shellwords'
9
+ require 'cabin'
10
+ require 'open3'
11
+ module FPM; module Fry
12
+
13
+ class Recipe
14
+
15
+ class PackageRecipe
16
+ attr_accessor :name,
17
+ :iteration,
18
+ :version,
19
+ :maintainer,
20
+ :vendor,
21
+ :depends,
22
+ :provides,
23
+ :conflicts,
24
+ :replaces,
25
+ :suggests,
26
+ :recommends,
27
+ :scripts,
28
+ :output_hooks,
29
+ :files
30
+
31
+
32
+ def initialize
33
+ @name = nil
34
+ @iteration = nil
35
+ @version = '0.0.0'
36
+ @maintainer = nil
37
+ @vendor = nil
38
+ @depends = {}
39
+ @provides = {}
40
+ @conflicts = {}
41
+ @replaces = {}
42
+ @scripts = {
43
+ before_install: [],
44
+ after_install: [],
45
+ before_remove: [],
46
+ after_remove: []
47
+ }
48
+ @output_hooks = []
49
+ @files = []
50
+ end
51
+
52
+ alias dependencies depends
53
+
54
+ def apply_output( package )
55
+ package.name = name
56
+ package.version = version
57
+ package.iteration = iteration
58
+ package.maintainer = maintainer if maintainer
59
+ package.vendor = vendor if vendor
60
+ scripts.each do |type, scripts|
61
+ package.scripts[type] = scripts.join("\n") if scripts.any?
62
+ end
63
+ [:dependencies, :conflicts, :replaces, :provides].each do |sym|
64
+ send(sym).each do |name, options|
65
+ package.send(sym) << "#{name}#{options[:version]}"
66
+ end
67
+ end
68
+ output_hooks.each{|h| h.call(self, package) }
69
+ return package
70
+ end
71
+
72
+ alias apply apply_output
73
+
74
+ SYNTAX_CHECK_SHELLS = ['/bin/sh','/bin/bash', '/bin/dash']
75
+
76
+ def lint
77
+ problems = []
78
+ problems << "Name is empty." if name.to_s == ''
79
+ scripts.each do |type,scripts|
80
+ next if scripts.none?
81
+ s = scripts.join("\n")
82
+ if s == ''
83
+ problems << "#{type} script is empty. This will produce broken packages."
84
+ next
85
+ end
86
+ m = /\A#!([^\n]+)\n/.match(s)
87
+ if !m
88
+ problems << "#{type} script doesn't have a valid shebang"
89
+ next
90
+ end
91
+ begin
92
+ args = m[1].shellsplit
93
+ rescue ArgumentError => e
94
+ problems << "#{type} script doesn't have a valid command in shebang"
95
+ end
96
+ if SYNTAX_CHECK_SHELLS.include? args[0]
97
+ sin, sout, serr, th = Open3.popen3(args[0],'-n')
98
+ sin.write(s)
99
+ sin.close
100
+ if th.value.exitstatus != 0
101
+ problems << "#{type} script is not valid #{args[0]} code: #{serr.read.chomp}"
102
+ end
103
+ serr.close
104
+ sout.close
105
+ end
106
+ end
107
+ return problems
108
+ end
109
+ end
110
+
111
+ attr_accessor :source, :steps, :packages, :build_depends, :input_hooks
112
+
113
+ def initialize
114
+ @source = Source::Null
115
+ @steps = {}
116
+ @packages = [PackageRecipe.new]
117
+ @packages[0].files << '**'
118
+ @build_depends = {}
119
+ @input_hooks = []
120
+ end
121
+
122
+ def depends
123
+ depends = @packages.map(&:depends).inject(:merge)
124
+ @packages.map(&:name).each do | n |
125
+ depends.delete(n)
126
+ end
127
+ return depends
128
+ end
129
+
130
+ def lint
131
+ packages.flat_map(&:lint)
132
+ end
133
+
134
+ def apply_input( package )
135
+ input_hooks.each{|h| h.call(self, package) }
136
+ return package
137
+ end
138
+
139
+ end
140
+
141
+ end ; end