cany 0.0.2 → 0.1.0
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.
- 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
|