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.
@@ -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