fpm-fry 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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