cany 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, nil] Returns the found class or nil if no class is registered on this
20
- # name
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 initialize(spec, inner)
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
- puts " #{args.flatten.join(' ')}"
51
- system *args.flatten
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, bundler, rake ...)
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] source The relative file name to a filename or directory inside the build
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] destination The diretory name into that the file or directory should be
92
+ # @param destination[String] The diretory name into that the file or directory should be
68
93
  # installed
69
- def install(src, dest_dir)
70
- exec 'dh_install', src, dest_dir
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
@@ -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}", "#!/bin/sh
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