cany 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/Gemfile +12 -0
- data/Guardfile +20 -0
- data/Rakefile +11 -0
- data/lib/cany.rb +88 -19
- data/lib/cany/dependency.rb +70 -0
- data/lib/cany/dpkg/builder.rb +4 -16
- data/lib/cany/dpkg/creator.rb +39 -29
- data/lib/cany/dpkg/deb_helper_recipe.rb +105 -20
- data/lib/cany/errors.rb +62 -0
- data/lib/cany/mixins/depend_mixin.rb +22 -0
- data/lib/cany/recipe.rb +182 -11
- data/lib/cany/recipes/bundler.rb +28 -5
- data/lib/cany/recipes/bundler/gem.rb +50 -0
- data/lib/cany/recipes/bundler/gem_db.rb +27 -0
- data/lib/cany/recipes/rails.rb +19 -2
- data/lib/cany/recipes/sidekiq.rb +38 -0
- data/lib/cany/recipes/thin.rb +24 -18
- data/lib/cany/recipes/unicorn.rb +16 -0
- data/lib/cany/recipes/web_server.rb +5 -19
- data/lib/cany/specification.rb +49 -1
- data/lib/cany/specification/dsl.rb +23 -3
- data/lib/cany/version.rb +2 -2
- data/spec/cany/dependency_spec.rb +120 -0
- data/spec/cany/dpkg/builder_spec.rb +38 -0
- data/spec/{dpkg → cany/dpkg}/creator_spec.rb +26 -2
- data/spec/cany/dpkg/deb_helper_recipe_spec.rb +91 -0
- data/spec/cany/recipe_spec.rb +213 -0
- data/spec/cany/recipes/bundler/gem_spec.rb +46 -0
- data/spec/cany/recipes/bundler_spec.rb +29 -0
- data/spec/cany/recipes/rails_spec.rb +28 -0
- data/spec/cany/recipes/sidekiq_spec.rb +52 -0
- data/spec/cany/specification_spec.rb +113 -1
- data/spec/spec_helper.rb +31 -4
- metadata +29 -6
- data/spec/dpkg/builder_spec.rb +0 -53
data/lib/cany/errors.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Cany
|
2
|
+
# Cany base error, design to catch all Cany errors at once
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
|
6
|
+
class MissingSpecification < Error
|
7
|
+
def initialize(directory)
|
8
|
+
super "No #{Specification::EXT} found in #{directory}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class MultipleSpecifications < Error
|
13
|
+
def initialize(directory)
|
14
|
+
super "Multiple #{Specification::EXT} found in #{directory}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class CommandExecutionFailed < Error
|
19
|
+
def initialize(args)
|
20
|
+
super "Could not execute: #{args.join(' ')}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UnknownHook < Error
|
25
|
+
def initialize(hook)
|
26
|
+
super "Unknown hook \"#{hook}\""
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class UnknownOption < Error
|
31
|
+
def initialize(option)
|
32
|
+
super "Unknown option \"#{option}\""
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class UnloadedRecipe < Error
|
37
|
+
def initialize(name)
|
38
|
+
super "The recipe \"#{name}\" is not loaded by the specification."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class UnknownRecipe < Error
|
43
|
+
def initialize(name)
|
44
|
+
super "The recipe \"#{name}\" is not registered!"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class NoSystemRecipe < Error
|
49
|
+
def initialize
|
50
|
+
super "The specification has no loaded system recipe."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# This exception is raised if the running Cany version satisfies not the
|
55
|
+
# required Cany version constraint from the canspec.
|
56
|
+
class UnsupportedVersion < Error
|
57
|
+
def initialize(required_version)
|
58
|
+
super "The package specification requires Cany in version" \
|
59
|
+
" \"#{required_version}\" but Cany has version \"#{Cany::VERSION}\""
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Cany::Mixins::DependMixin
|
2
|
+
# @overload depend(dep)
|
3
|
+
# @param depend[Cany::Dependency] A complete Dependency object
|
4
|
+
# @overload depend(default, opts)
|
5
|
+
# Creates a new dependency object
|
6
|
+
# @param depend[Symbol] The default
|
7
|
+
# @param opts[Hash] Options influencing the create Dependency object.
|
8
|
+
# @option opts[Symbol, Array<Symbol>] :situation For which situations
|
9
|
+
# is this dependency. Default is :runtime
|
10
|
+
# @option opts[Symbol] :version The default version
|
11
|
+
def create_dep(depend, opts={})
|
12
|
+
if depend.kind_of? Cany::Dependency
|
13
|
+
depend
|
14
|
+
else
|
15
|
+
opts = { situation: :runtime, version: nil }.merge opts
|
16
|
+
dep = Cany::Dependency.new
|
17
|
+
dep.define_default depend, opts[:version]
|
18
|
+
dep.situations = opts[:situation]
|
19
|
+
dep
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/cany/recipe.rb
CHANGED
@@ -11,23 +11,41 @@ module Cany
|
|
11
11
|
def self.register_as(name)
|
12
12
|
@@recipes ||= {}
|
13
13
|
@@recipes[name] = self
|
14
|
+
module_eval(<<-EOS, __FILE__, __LINE__)
|
15
|
+
def name
|
16
|
+
:#{name}
|
17
|
+
end
|
18
|
+
EOS
|
14
19
|
end
|
15
20
|
|
16
21
|
# @api public
|
17
22
|
# Looks for the class registered for the given name
|
18
23
|
# @param [Symbol] name the name the class is search for
|
19
|
-
# @return [Cany::Recipe
|
20
|
-
#
|
24
|
+
# @return [Cany::Recipe] Returns the found class or nil
|
25
|
+
# @raise UnknownRecipe if there is no recipe registered for this name.
|
21
26
|
def self.from_name(name)
|
27
|
+
raise UnknownRecipe.new(name) unless @@recipes[name]
|
22
28
|
@@recipes[name]
|
23
29
|
end
|
24
30
|
|
25
31
|
# Creates a new instance of this recipe
|
26
32
|
# @param [Cany::Specification] spec Specification object
|
33
|
+
def initialize(spec, &configure_block)
|
34
|
+
@spec = spec
|
35
|
+
@inner = nil
|
36
|
+
@hooks = Hash[(self.class.defined_hooks || []).map do |name|
|
37
|
+
[name, Cany.hash_with_array_as_default]
|
38
|
+
end]
|
39
|
+
@options = Hash[(self.class.defined_options || []).map do |name|
|
40
|
+
[name, Cany.hash_with_array_as_default]
|
41
|
+
end]
|
42
|
+
self.class.const_get(:DSL).new(self).exec(&configure_block) if configure_block
|
43
|
+
end
|
44
|
+
|
45
|
+
# Specify the inner recipe for the current one.
|
27
46
|
# @param [Cany::Recipe, nil] inner Inner recipes should should be call between the pre and post
|
28
47
|
# actions of this class. Nil means most inner recipes.
|
29
|
-
def
|
30
|
-
@spec = spec
|
48
|
+
def inner=(inner)
|
31
49
|
@inner = inner
|
32
50
|
end
|
33
51
|
|
@@ -46,13 +64,20 @@ module Cany
|
|
46
64
|
# exec 'echo', %w(a b)
|
47
65
|
# exec ['echo', 'a', 'b']
|
48
66
|
# exec 'echo', 'a', 'b'
|
67
|
+
# @raise [CommandExecutionFailed] if the executed program exists with a
|
68
|
+
# non-zero exit code.
|
49
69
|
def exec(*args)
|
50
|
-
|
51
|
-
|
70
|
+
args.flatten!
|
71
|
+
Cany.logger.info args.join(' ')
|
72
|
+
unless system(*args)
|
73
|
+
raise CommandExecutionFailed.new args
|
74
|
+
end
|
52
75
|
end
|
76
|
+
# exec is special name in same situations it may no work but this alias should work always
|
77
|
+
alias :exec_ :exec
|
53
78
|
|
54
79
|
# @api public
|
55
|
-
# Run a ruby task (like gem,
|
80
|
+
# Run a ruby task (like gem, bundle, rake ...)
|
56
81
|
#
|
57
82
|
# The method expects as arguments the program name and additional parameters for the program.
|
58
83
|
# See exec for more examples
|
@@ -62,12 +87,12 @@ module Cany
|
|
62
87
|
|
63
88
|
# @api public
|
64
89
|
# Install files or directory from the build directory
|
65
|
-
# @param [String]
|
90
|
+
# @param source[String] The relative file name to a filename or directory inside the build
|
66
91
|
# directory that should be installed/copied into the destination package
|
67
|
-
# @param [String]
|
92
|
+
# @param destination[String] The diretory name into that the file or directory should be
|
68
93
|
# installed
|
69
|
-
def install(
|
70
|
-
exec 'dh_install',
|
94
|
+
def install(source, destination)
|
95
|
+
exec 'dh_install', source, destination
|
71
96
|
end
|
72
97
|
|
73
98
|
# @api public
|
@@ -95,6 +120,21 @@ module Cany
|
|
95
120
|
exec 'dh_link', source, destination
|
96
121
|
end
|
97
122
|
|
123
|
+
# @api public
|
124
|
+
# Specify a command call (program + args) that should be installed as service and started
|
125
|
+
# automatically.
|
126
|
+
# This method should be only call inside the binary step.
|
127
|
+
# @param name[Symbol] A short identifier. Used to separate different services. E.g. the name
|
128
|
+
# of the web server that is launched by this command (like puma, unicorn, thin)
|
129
|
+
# @param command[Array<String>] The command that should be started and its parameter. The first
|
130
|
+
# element is the command name - can be absolute or relative path name (than searched in path)
|
131
|
+
# @param opts[Hash] Service behavior options
|
132
|
+
# @option opts[String] :user As which user should the command executed (default is root)
|
133
|
+
# @option opts[String] :group As which group should the command executed (default is root)
|
134
|
+
def install_service(*args)
|
135
|
+
recipe(:system).install_service(*args)
|
136
|
+
end
|
137
|
+
|
98
138
|
# @api public
|
99
139
|
# Ensure that the given files or directories are no present. Directories are removed
|
100
140
|
# recursively.
|
@@ -104,9 +144,100 @@ module Cany
|
|
104
144
|
end
|
105
145
|
end
|
106
146
|
|
147
|
+
class << self
|
148
|
+
attr_accessor :defined_hooks, :defined_options
|
149
|
+
|
150
|
+
# @api public
|
151
|
+
# Define a new hook
|
152
|
+
# @param name[Symbol]
|
153
|
+
def hook(name)
|
154
|
+
@defined_hooks ||= []
|
155
|
+
@defined_hooks << name
|
156
|
+
end
|
157
|
+
|
158
|
+
# @api public
|
159
|
+
# Define a configure option. These kind of option are design for other
|
160
|
+
# recipes not for the user. See Recipe::DSL for this.
|
161
|
+
# @param name[Symbol] The name of the option. The option name is scoped
|
162
|
+
# inside a recipe.
|
163
|
+
def option(name)
|
164
|
+
@defined_options ||= []
|
165
|
+
@defined_options << name
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def hook(name)
|
170
|
+
@hooks[name].tap do |hook|
|
171
|
+
raise UnknownHook.new name unless hook
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# @api public
|
176
|
+
# Ask for the current values for a defined option
|
177
|
+
def option(name)
|
178
|
+
@options[name].tap do |option|
|
179
|
+
raise UnknownOption.new name unless option
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# @api public
|
184
|
+
# Configure an other recipe
|
185
|
+
# @param name[Symbol] The option name
|
186
|
+
# @param options[Hash] The configuration data itself.
|
187
|
+
def configure(name, options)
|
188
|
+
option(name).merge! options
|
189
|
+
end
|
190
|
+
|
191
|
+
# @api public
|
192
|
+
# Run defined actions for a hook
|
193
|
+
# @param name[Symbol] hook identification, no error is raised on unknown hooks
|
194
|
+
# @param state[Symbol] state that should be executed (:before, :after or :around)
|
195
|
+
def run_hook(name, state)
|
196
|
+
hook(name)[state].each do |block|
|
197
|
+
Cany.logger.info "run #{block} for hook #{name} in state #{state} ..."
|
198
|
+
instance_eval(&block)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# @api public
|
203
|
+
# Access the recipe instance from another loaded recipe of this
|
204
|
+
# specification
|
205
|
+
# @param name[Symbol] recipe name
|
206
|
+
def recipe(name)
|
207
|
+
return spec.system_recipe if name == :system
|
208
|
+
recipe_class = Recipe.from_name(name)
|
209
|
+
@spec.recipes.each do |one_recipe|
|
210
|
+
return one_recipe if one_recipe.instance_of? recipe_class
|
211
|
+
end
|
212
|
+
raise UnloadedRecipe.new name
|
213
|
+
end
|
214
|
+
|
215
|
+
# @api public
|
216
|
+
# Adds a new dependency for the software. See Cany::Dependency for a more
|
217
|
+
# abstract description about dependencies
|
218
|
+
# See Cany::Mixins::DependMixin for parameter description
|
219
|
+
include Cany::Mixins::DependMixin
|
220
|
+
def depend(*args)
|
221
|
+
@spec.dependencies << create_dep(*args)
|
222
|
+
end
|
223
|
+
|
107
224
|
# default implementation:
|
108
225
|
#########################
|
109
226
|
|
227
|
+
# @!group Recipe Steps - to be overridden in subclass
|
228
|
+
|
229
|
+
# @api public
|
230
|
+
# Prepares the recipes to run things. This is call exactly once for all recipes before
|
231
|
+
# recipes actions are executed.
|
232
|
+
def prepare
|
233
|
+
end
|
234
|
+
|
235
|
+
# @api public
|
236
|
+
# This step is executed to create the distribution specific packages from canspec. The recipe
|
237
|
+
# can e.g. add additional dependencies or adjust the package meta data.
|
238
|
+
def create(creator)
|
239
|
+
end
|
240
|
+
|
110
241
|
# @api public
|
111
242
|
# clean the build directory from all temporary and created files
|
112
243
|
def clean
|
@@ -124,5 +255,45 @@ module Cany
|
|
124
255
|
def binary
|
125
256
|
inner.binary
|
126
257
|
end
|
258
|
+
|
259
|
+
# @!endgroup
|
260
|
+
|
261
|
+
|
262
|
+
# This superclass helps recipes to create easily an own mini DSL to let the user configure the
|
263
|
+
# recipe with it.
|
264
|
+
class DSL
|
265
|
+
def initialize(recipe)
|
266
|
+
@recipe = recipe
|
267
|
+
end
|
268
|
+
|
269
|
+
# Evaluate a given block inside the dsl.
|
270
|
+
def exec(&block)
|
271
|
+
instance_eval(&block)
|
272
|
+
end
|
273
|
+
|
274
|
+
# This is a simple delegate helper. It can be used to pass option directly to recipe instance.
|
275
|
+
# @param [Symbol] param1 Multiple symbol names
|
276
|
+
def self.delegate(*methods)
|
277
|
+
methods.each do |method|
|
278
|
+
module_eval(<<-EOS, __FILE__, __LINE__)
|
279
|
+
def #{method}(*args, &block)
|
280
|
+
@recipe.send :'#{method}=', *args, &block
|
281
|
+
end
|
282
|
+
EOS
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def before(hook_name, &block)
|
287
|
+
@recipe.hook(hook_name)[:before] << block
|
288
|
+
end
|
289
|
+
|
290
|
+
def after(hook_name, &block)
|
291
|
+
@recipe.hook(hook_name)[:after] << block
|
292
|
+
end
|
293
|
+
|
294
|
+
def around(hook_name, &block)
|
295
|
+
@recipe.hook(hook_name)[:around] << block
|
296
|
+
end
|
297
|
+
end
|
127
298
|
end
|
128
299
|
end
|
data/lib/cany/recipes/bundler.rb
CHANGED
@@ -2,6 +2,7 @@ module Cany
|
|
2
2
|
module Recipes
|
3
3
|
class Bundler < Cany::Recipe
|
4
4
|
register_as :bundler
|
5
|
+
option :env_vars
|
5
6
|
|
6
7
|
def clean
|
7
8
|
rmtree 'bundler'
|
@@ -9,6 +10,23 @@ module Cany
|
|
9
10
|
inner.clean
|
10
11
|
end
|
11
12
|
|
13
|
+
def prepare
|
14
|
+
configure :env_vars, GEM_PATH: 'bundler'
|
15
|
+
end
|
16
|
+
|
17
|
+
def create(creator)
|
18
|
+
require 'bundler'
|
19
|
+
lock_path = File.join(spec.base_dir, 'Gemfile.lock')
|
20
|
+
if File.exists? lock_path
|
21
|
+
lock = ::Bundler::LockfileParser.new File.read lock_path
|
22
|
+
lock.specs.each do |spec|
|
23
|
+
Gem.get(spec.name.to_sym).dependencies.each do |dep|
|
24
|
+
depend dep
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
12
30
|
def build
|
13
31
|
ENV['GEM_PATH'] = 'bundler'
|
14
32
|
ENV['PATH'] = 'bundler/bin:' + ENV['PATH']
|
@@ -21,13 +39,18 @@ module Cany
|
|
21
39
|
install 'bundler', "/usr/share/#{spec.name}"
|
22
40
|
install '.bundle', "/usr/share/#{spec.name}"
|
23
41
|
install 'vendor/bundle', "/usr/share/#{spec.name}/vendor"
|
24
|
-
install_content "/usr/bin/#{spec.name}",
|
25
|
-
cd /usr/share/#{spec.name}
|
26
|
-
export GEM_PATH=/usr/share/#{spec.name}/bundler
|
27
|
-
exec /usr/share/#{spec.name}/bundler/bin/bundle exec \"$@\"
|
28
|
-
"
|
42
|
+
install_content "/usr/bin/#{spec.name}", wrapper_script
|
29
43
|
inner.binary
|
30
44
|
end
|
45
|
+
|
46
|
+
def wrapper_script
|
47
|
+
content = [ '#!/bin/sh', "cd /usr/share/#{spec.name}" ]
|
48
|
+
option(:env_vars).each do |name, value|
|
49
|
+
content << "export #{name}=\"#{value}\""
|
50
|
+
end
|
51
|
+
content += [ "exec /usr/share/#{spec.name}/bundler/bin/bundle exec \"$@\"", '' ]
|
52
|
+
content.join "\n"
|
53
|
+
end
|
31
54
|
end
|
32
55
|
end
|
33
56
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Cany::Recipes::Bundler::Gem
|
2
|
+
# @api public
|
3
|
+
# This methods returns the Gem instance for the specified gem
|
4
|
+
# @param gem_name[Symbol] The gem name
|
5
|
+
# @return Cany::recipes::Bundler::Gem
|
6
|
+
def self.get(gem_name)
|
7
|
+
@gems ||= {}
|
8
|
+
@gems[gem_name] ||= new gem_name
|
9
|
+
end
|
10
|
+
|
11
|
+
# @api public
|
12
|
+
# Specify meta data about the given gem
|
13
|
+
# @param gem_name[Symbol] the gem name
|
14
|
+
# @block: A block that defines the meta data. Executed inside the DSL subclass
|
15
|
+
def self.specify(gem_name, &block)
|
16
|
+
DSL.new(get(gem_name)).run &block
|
17
|
+
end
|
18
|
+
|
19
|
+
# Clear all stored data. Only used in rspec
|
20
|
+
def self.clear
|
21
|
+
@gems = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :name, :dependencies
|
25
|
+
|
26
|
+
private
|
27
|
+
def initialize(gem_name)
|
28
|
+
@name = gem_name
|
29
|
+
@dependencies = []
|
30
|
+
end
|
31
|
+
|
32
|
+
class DSL
|
33
|
+
def initialize(gem)
|
34
|
+
@gem = gem
|
35
|
+
end
|
36
|
+
|
37
|
+
def run(&block)
|
38
|
+
instance_eval(&block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @api public
|
42
|
+
# Adds a new dependency for the software. See Cany::Dependency for a more
|
43
|
+
# abstract description about dependencies
|
44
|
+
# See Cany::Mixins::DependMixin for parameter description
|
45
|
+
include Cany::Mixins::DependMixin
|
46
|
+
def depend(*args)
|
47
|
+
@gem.dependencies << create_dep(*args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Cany::Recipes::Bundler
|
2
|
+
Gem.specify :charlock_holmes do
|
3
|
+
depend 'libicu-dev', situation: :build
|
4
|
+
depend 'libicu48', situation: :runtime
|
5
|
+
end
|
6
|
+
|
7
|
+
Gem.specify :ethon do
|
8
|
+
depend 'libcurl3-gnutls', situation: [:build, :runtime]
|
9
|
+
end
|
10
|
+
|
11
|
+
Gem.specify :mysql2 do
|
12
|
+
depend 'libmysqlclient-dev', situation: :build
|
13
|
+
depend 'libmysqlclient18', situation: :runtime
|
14
|
+
end
|
15
|
+
|
16
|
+
Gem.specify :nokogiri do
|
17
|
+
depend 'libxml2-dev', situation: :build
|
18
|
+
depend 'libxml2', situation: :runtime
|
19
|
+
depend 'libxslt1-dev', situation: :build
|
20
|
+
depend 'libxslt1.1', situation: :runtime
|
21
|
+
end
|
22
|
+
|
23
|
+
Gem.specify :pg do
|
24
|
+
depend 'libpq-dev', situation: :build
|
25
|
+
depend 'libpq5', situation: :runtime
|
26
|
+
end
|
27
|
+
end
|