linecook 1.2.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/{History → History.rdoc} +3 -2
  2. data/README.rdoc +93 -0
  3. data/bin/linecook +32 -56
  4. data/bin/linecook_run +19 -6
  5. data/bin/linecook_scp +12 -4
  6. data/doc/vm_setup.rdoc +75 -0
  7. data/lib/linecook.rb +3 -2
  8. data/lib/linecook/attributes.rb +33 -8
  9. data/lib/linecook/command.rb +61 -0
  10. data/lib/linecook/command_set.rb +85 -0
  11. data/lib/linecook/command_utils.rb +20 -0
  12. data/lib/linecook/commands/build.rb +108 -57
  13. data/lib/linecook/commands/compile.rb +181 -0
  14. data/lib/linecook/commands/{helper.rb → compile_helper.rb} +123 -94
  15. data/lib/linecook/commands/run.rb +43 -39
  16. data/lib/linecook/commands/snapshot.rb +24 -24
  17. data/lib/linecook/commands/ssh.rb +7 -7
  18. data/lib/linecook/commands/start.rb +10 -10
  19. data/lib/linecook/commands/state.rb +7 -7
  20. data/lib/linecook/commands/stop.rb +3 -3
  21. data/lib/linecook/commands/{vbox_command.rb → virtual_box_command.rb} +31 -29
  22. data/lib/linecook/cookbook.rb +149 -131
  23. data/lib/linecook/executable.rb +28 -0
  24. data/lib/linecook/package.rb +177 -361
  25. data/lib/linecook/proxy.rb +4 -10
  26. data/lib/linecook/recipe.rb +289 -369
  27. data/lib/linecook/test.rb +114 -98
  28. data/lib/linecook/utils.rb +31 -41
  29. data/lib/linecook/version.rb +2 -6
  30. metadata +120 -68
  31. data/HowTo/Control Virtual Machines +0 -106
  32. data/HowTo/Generate Scripts +0 -268
  33. data/HowTo/Run Scripts +0 -87
  34. data/HowTo/Setup Virtual Machines +0 -76
  35. data/README +0 -117
  36. data/lib/linecook/commands.rb +0 -11
  37. data/lib/linecook/commands/command.rb +0 -58
  38. data/lib/linecook/commands/command_error.rb +0 -12
  39. data/lib/linecook/commands/env.rb +0 -89
  40. data/lib/linecook/commands/init.rb +0 -86
  41. data/lib/linecook/commands/package.rb +0 -57
  42. data/lib/linecook/template.rb +0 -17
  43. data/lib/linecook/test/command_parser.rb +0 -75
  44. data/lib/linecook/test/file_test.rb +0 -197
  45. data/lib/linecook/test/regexp_escape.rb +0 -86
  46. data/lib/linecook/test/shell_test.rb +0 -177
  47. data/lib/linecook/test/shim.rb +0 -71
  48. data/templates/Gemfile +0 -3
  49. data/templates/Rakefile +0 -146
  50. data/templates/_gitignore +0 -4
  51. data/templates/attributes/project_name.rb +0 -3
  52. data/templates/config/ssh +0 -14
  53. data/templates/cookbook +0 -10
  54. data/templates/files/example.txt +0 -1
  55. data/templates/helpers/project_name/echo.erb +0 -4
  56. data/templates/packages/abox.yml +0 -2
  57. data/templates/project_name.gemspec +0 -30
  58. data/templates/recipes/abox.rb +0 -16
  59. data/templates/templates/example.erb +0 -1
  60. data/templates/test/project_name_test.rb +0 -24
  61. data/templates/test/test_helper.rb +0 -14
@@ -0,0 +1,61 @@
1
+ require 'configurable'
2
+
3
+ module Linecook
4
+ class Command
5
+ class << self
6
+ def parse(argv=ARGV)
7
+ parser = configs.to_parser(:add_defaults => false)
8
+ yield(parser) if block_given?
9
+ parser.sort_opts!
10
+ parser.parse!(argv)
11
+
12
+ new(parser.config)
13
+ end
14
+
15
+ def signature
16
+ arguments = args.arguments.collect do |arg|
17
+ arg = arg.upcase
18
+ arg[0] == ?* ? "[#{arg[1..-1]}]" : arg
19
+ end
20
+ arguments.pop if arguments.last.to_s[0] == ?&
21
+
22
+ "[options] #{arguments.join(' ')}"
23
+ end
24
+
25
+ # Returns a help string that formats the desc documentation.
26
+ def help
27
+ lines = desc.kind_of?(Lazydoc::Comment) ? desc.wrap(78, 2, nil) : []
28
+
29
+ unless lines.empty?
30
+ line = '-' * 80
31
+ lines.unshift(line)
32
+ lines.push(line)
33
+ end
34
+
35
+ lines.join("\n")
36
+ end
37
+ end
38
+
39
+ extend Lazydoc::Attributes
40
+ include Configurable
41
+
42
+ lazy_attr :desc
43
+ lazy_attr :args, :process
44
+ lazy_register :process, Lazydoc::Method
45
+
46
+ def initialize(config={})
47
+ initialize_config(config)
48
+ end
49
+
50
+ def call(argv=[])
51
+ process(*argv)
52
+ end
53
+
54
+ def process(*args)
55
+ raise NotImplementedError
56
+ end
57
+ end
58
+
59
+ class CommandError < RuntimeError
60
+ end
61
+ end
@@ -0,0 +1,85 @@
1
+ require 'linecook/command'
2
+
3
+ module Linecook
4
+ class CommandSet < Command
5
+ class << self
6
+ def commands
7
+ # assume/require word names
8
+ @commands ||= {}
9
+ end
10
+
11
+ def parse(argv=ARGV)
12
+ command = super(argv) do |options|
13
+ options.option_break = /\A(?:--\z|[^-])/
14
+ options.preserve_option_break = true
15
+ yield(options) if block_given?
16
+ end
17
+
18
+ # The parser is configured to preserve the break, which is desired
19
+ # when it's a command, but you want to get rid of standard breaks.
20
+ if argv[0] == '--'
21
+ argv.shift
22
+ end
23
+
24
+ command
25
+ end
26
+
27
+ def run(argv=ARGV, &block)
28
+ command = parse(argv) do |options|
29
+ if block_given?
30
+ yield([], self, options)
31
+ end
32
+ end
33
+
34
+ command.call(argv, &block)
35
+ end
36
+
37
+ def command_list
38
+ commands.keys.sort.collect do |name|
39
+ [name, commands[name]]
40
+ end
41
+ end
42
+ end
43
+
44
+ def call(argv=[])
45
+ if argv.empty?
46
+ raise CommandError, "no command specified"
47
+ end
48
+
49
+ command_name = argv.shift
50
+ command_class = self.class.commands[command_name]
51
+
52
+ unless command_class
53
+ raise CommandError, "unknown command: #{command_name.inspect}"
54
+ end
55
+
56
+ # Parse options for the command, but yield with the necessary debugging
57
+ # information - note that command_class is always the latest one to be
58
+ # parsed so start with the command name as the callpath.
59
+ command = command_class.parse(argv) do |options|
60
+ if block_given?
61
+ yield([command_name], command_class, options)
62
+ end
63
+ end
64
+
65
+ if command.kind_of?(CommandSet)
66
+ # The block causes nested CommandSets to roll back out to the main
67
+ # context with the callpath and latest command_class. Literally it
68
+ # wraps the block defined above. Non-CommandSet commands will have
69
+ # nil, not a block passed to call - a NOP.
70
+ command.call(argv) do |callpath, cmdclass, options|
71
+ if block_given?
72
+ callpath.unshift(command_name)
73
+ yield(callpath, cmdclass, options)
74
+ end
75
+ end
76
+ else
77
+ process(command, *argv)
78
+ end
79
+ end
80
+
81
+ def process(command, *args)
82
+ command.call(args)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,20 @@
1
+ require 'configurable'
2
+
3
+ module Linecook
4
+ module CommandUtils
5
+ include Configurable
6
+
7
+ config :quiet, false # -q, --quiet : silence output
8
+
9
+ def sh(cmd)
10
+ $stderr.puts "$ #{cmd}" unless quiet
11
+ system(cmd)
12
+ end
13
+
14
+ def sh!(cmd)
15
+ unless sh(cmd)
16
+ raise CommandError, "non-zero exit status: #{$?.exitstatus}"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,72 +1,123 @@
1
- require 'linecook/commands/helper'
2
- require 'linecook/commands/package'
1
+ require 'linecook/commands/compile'
3
2
 
4
3
  module Linecook
5
4
  module Commands
6
-
7
- # :startdoc::desc build a project
5
+ # ::desc build packages
8
6
  #
9
- # Builds some or all packages and helpers in a project, as needed.
7
+ # Builds a list of 'package' recipes into packages. Packages are exported
8
+ # to a directory named like the recipe. Build prints the package dir for
9
+ # each recipe to stdout.
10
10
  #
11
- class Build < Command
12
- config :project_dir, '.', :short => :d # the project directory
13
- config :load_path, [], :short => :I, &c.list # set load paths
14
- config :force, false, :short => :f, &c.flag # force creation
15
- config :quiet, false, &c.flag # silence output
16
-
17
- def glob_helpers(project_dir)
18
- helpers_dir = File.expand_path('helpers', project_dir)
19
- sources = {}
20
- helpers = []
21
-
22
- Dir.glob("#{helpers_dir}/*/**/*").each do |source_file|
23
- next if File.directory?(source_file)
24
- (sources[File.dirname(source_file)] ||= []) << source_file
25
- end
26
-
27
- sources.each_pair do |dir, source_files|
28
- name = dir[(helpers_dir.length + 1)..-1]
29
- helpers << [name, source_files]
11
+ # Recipes are added to the package as the 'run' executable and they are
12
+ # automatically configured with a package file corresponding to the
13
+ # recipe, if it exists.
14
+ #
15
+ # For example:
16
+ #
17
+ # $ echo "write 'echo ' + attrs['msg']" > recipe.rb
18
+ # $ echo "msg: hello world" > recipe.yml
19
+ # $ linecook build recipe.rb
20
+ # /path/to/pwd/recipe
21
+ # $ /path/to/pwd/recipe/run
22
+ # hello world
23
+ #
24
+ # The input directory containing the package files and the output
25
+ # directory for the packages may be specified with options.
26
+ #
27
+ # == Package Specs
28
+ #
29
+ # Package specs can be provided instead of recipes. Specs are
30
+ # comma-separated strings like 'package_file,recipe_file,export_dir' that
31
+ # allow full control over the building of packages. Package files
32
+ #
33
+ # For example:
34
+ #
35
+ # $ echo "write 'echo ' + attrs['msg']" > recipe.rb
36
+ # $ echo "msg: hello world" > input.yml
37
+ # $ linecook build input.yml,recipe.rb,output
38
+ # /path/to/pwd/output
39
+ # $ /path/to/pwd/output/run
40
+ # hello world
41
+ #
42
+ # Providing '-' as an input will cause stdin to be read for additional
43
+ # inputs. In that way a CSV file can serve as a manifest for the packages
44
+ # built by this command.
45
+ #
46
+ class Build < Compile
47
+ class << self
48
+ def set_common_options(options)
49
+ super(options)
50
+ options[:input_dir] = 'packages'
51
+ options[:output_dir] = 'packages'
52
+ options
30
53
  end
31
-
32
- helpers.sort_by {|name, source_files| name }
33
54
  end
34
-
35
- def glob_package_files(package_names)
36
- if package_names.empty?
37
- pattern = File.expand_path('packages/*.yml', project_dir)
38
- Dir.glob(pattern).select {|path| File.file?(path) }
39
- else
40
- package_names.collect do |package_name|
41
- File.expand_path("packages/#{package_name}.yml", project_dir)
55
+ undef_config :package_file
56
+
57
+ def input_dir=(input)
58
+ @package_finder = nil
59
+ super(input)
60
+ end
61
+
62
+ def process(*recipes)
63
+ helper_dirs.each do |helpers_dir|
64
+ compile_helpers(helpers_dir)
65
+ end
66
+
67
+ each_spec(recipes) do |package_file, recipe_file, export_dir|
68
+ export_dir = File.expand_path(export_dir, output_dir)
69
+ if File.exists?(export_dir)
70
+ unless force
71
+ raise CommandError, "already exists: #{export_dir.inspect}"
72
+ end
73
+ FileUtils.rm_r(export_dir)
42
74
  end
75
+
76
+ package_file = File.expand_path(package_file, input_dir)
77
+ recipe_file = File.expand_path(recipe_file)
78
+
79
+ package = Package.new(load_env(package_file))
80
+ cookbook = Cookbook.new(*cookbook_path)
81
+ recipe = Recipe.new(package, cookbook)
82
+ recipe.register_as 'run', :mode => 0744
83
+ recipe.instance_eval File.read(recipe_file), recipe_file
84
+ recipe.register_to package
85
+
86
+ package.export(export_dir)
87
+ puts export_dir
43
88
  end
44
89
  end
45
-
46
- def process(*package_names)
47
- load_path.each do |path|
48
- $:.unshift(path)
90
+
91
+ def each_line(lines)
92
+ lines.each do |line|
93
+ if line == '-'
94
+ while line = gets
95
+ yield line
96
+ end
97
+ else
98
+ yield line
99
+ end
49
100
  end
50
-
51
- helper = Helper.new(
52
- :project_dir => project_dir,
53
- :force => force,
54
- :quiet => true
55
- )
56
-
57
- helpers = glob_helpers(project_dir)
58
- helpers.each do |(name, sources)|
59
- helper.process(name, *sources)
101
+ end
102
+
103
+ def each_spec(lines)
104
+ each_line(lines) do |line|
105
+ yield *parse_spec(line)
60
106
  end
61
-
62
- package = Package.new(
63
- :project_dir => project_dir,
64
- :force => force,
65
- :quiet => quiet
66
- )
67
-
68
- glob_package_files(package_names).collect do |package_file|
69
- package.process(package_file)
107
+ end
108
+
109
+ def parse_spec(spec)
110
+ fields = CSV.parse_line(spec)
111
+
112
+ case fields.length
113
+ when 1 # short form
114
+ recipe_path = fields.at(0)
115
+ base_name = File.basename(recipe_path).chomp('.rb')
116
+ ["#{base_name}.yml", recipe_path, base_name]
117
+ when 3 # long form
118
+ fields
119
+ else
120
+ raise "invalid spec: #{spec.inspect}"
70
121
  end
71
122
  end
72
123
  end
@@ -0,0 +1,181 @@
1
+ require 'fileutils'
2
+ require 'linecook/recipe'
3
+ require 'linecook/commands/compile_helper'
4
+ require 'yaml'
5
+ require 'csv'
6
+
7
+ module Linecook
8
+ module Commands
9
+ # ::desc compile recipes
10
+ #
11
+ # Compiles a list of recipes into a single package and exports the result
12
+ # to the working directory. The recipes are added to the package at their
13
+ # relative path, minus their extname.
14
+ #
15
+ # For example:
16
+ #
17
+ # $ echo "write 'echo hello world'" > recipe.rb
18
+ # $ linecook compile recipe.rb
19
+ # $ sh recipe
20
+ # hello world
21
+ #
22
+ # Providing '-' as a recipe will cause stdin to be compiled as a recipe to
23
+ # stdout.
24
+ class Compile < Command
25
+ class << self
26
+ def parse(argv=ARGV)
27
+ super(argv) do |options|
28
+ options.on('-I DIRECTORY', 'prepend to LOAD_PATH') do |path|
29
+ $LOAD_PATH.unshift File.expand_path(path)
30
+ end
31
+
32
+ options.on('-r LIBRARY', 'require the library') do |path|
33
+ require(path)
34
+ end
35
+
36
+ options.on('-G GEMNAME', 'add gem to cookbook path', :option_type => :list) do |name|
37
+ specs = Gem.source_index.find_name(name)
38
+ if specs.empty?
39
+ raise CommandError, "could not find gem: #{name.inspect}"
40
+ end
41
+ (options[:cookbook_path] ||= []) << specs.first.full_gem_path
42
+ end
43
+
44
+ options.on('-g', '--gems', 'add latest cookbook gems') do |name|
45
+ (options[:cookbook_path] ||= []).concat cookbook_gem_paths
46
+ end
47
+
48
+ options.on('-c', '--common', 'use common flags') do
49
+ set_common_options(options)
50
+ end
51
+
52
+ if block_given?
53
+ yield(options)
54
+ end
55
+ end
56
+ end
57
+
58
+ def set_common_options(options)
59
+ cookbook_path = (options[:cookbook_path] ||= [])
60
+ cookbook_path << '.'
61
+ cookbook_path.concat cookbook_gem_paths
62
+ (options[:helper_dirs] ||= []) << 'helpers'
63
+ options
64
+ end
65
+
66
+ def cookbook_gem_paths(latest=true)
67
+ Cookbook.gemspecs(latest).collect do |gemspec|
68
+ gemspec.full_gem_path
69
+ end
70
+ end
71
+
72
+ def gemspecs(latest=true)
73
+ return [] unless Object.const_defined?(:Gem)
74
+
75
+ index = Gem.source_index
76
+ specs = latest ? index.latest_specs : index.gems.values
77
+
78
+ specs.select do |spec|
79
+ cookbook_file = File.expand_path(default_file_name, spec.full_gem_path)
80
+ File.exists?(cookbook_file)
81
+ end
82
+ end
83
+ end
84
+
85
+ config :cookbook_path, [], :delimiter => ':' # -C PATH : cookbook path
86
+ config :helper_dirs, [] # -L DIRECTORY : compile helpers
87
+ config :package_file, nil # -p PATH : package config file
88
+ config :input_dir, '.', :writer => :input_dir= # -i DIRECTORY : the input dir
89
+ config :output_dir, '.', :writer => :output_dir= # -o DIRECTORY : the output dir
90
+ config :force, false # -f, --force : overwrite existing
91
+
92
+ def input_dir=(input)
93
+ @input_dir = File.expand_path(input)
94
+ end
95
+
96
+ def output_dir=(input)
97
+ @output_dir = File.expand_path(input)
98
+ end
99
+
100
+ def process(*recipes)
101
+ helper_dirs.each do |helper_dir|
102
+ compile_helpers(helper_dir)
103
+ end
104
+
105
+ package = Package.new(load_env(package_file))
106
+ cookbook = Cookbook.new(*cookbook_path)
107
+ stdout = StringIO.new
108
+
109
+ recipes.each do |recipe_path|
110
+ recipe = Recipe.new(package, cookbook)
111
+
112
+ if recipe_path == '-'
113
+ recipe.instance_eval $stdin.read, 'stdin'
114
+ stdout.print recipe
115
+ else
116
+ recipe_path = File.expand_path(recipe_path)
117
+ recipe.register_as relative_path(input_dir, recipe_path).chomp('.rb')
118
+ recipe.instance_eval File.read(recipe_path), recipe_path
119
+ recipe.register_to package
120
+ end
121
+ end
122
+
123
+ package.export(output_dir) do |src, dest|
124
+ unless force
125
+ raise CommandError, "already exists: #{dest.inspect}"
126
+ end
127
+
128
+ FileUtils.rm_rf(dest)
129
+ true
130
+ end
131
+
132
+ # print to $stdout after export to ensure files will be available
133
+ $stdout << stdout.string
134
+ end
135
+
136
+ def relative_path(dir, path)
137
+ if path.index(dir) == 0 && path != dir
138
+ path[dir.length + 1, path.length - dir.length]
139
+ else
140
+ File.basename(path)
141
+ end
142
+ end
143
+
144
+ def load_env(package_file)
145
+ env = package_file && File.exists?(package_file) ? YAML.load_file(package_file) : nil
146
+ env.nil? ? {} : env
147
+ end
148
+
149
+ def glob_helpers(helper_dir)
150
+ sources = {}
151
+ helpers = []
152
+
153
+ Dir.glob("#{helper_dir}/*/**/*").each do |source_file|
154
+ next if File.directory?(source_file)
155
+ (sources[File.dirname(source_file)] ||= []) << source_file
156
+ end
157
+
158
+ sources.each_pair do |dir, source_files|
159
+ name = dir[(helper_dir.length + 1)..-1]
160
+ helpers << [name, source_files]
161
+ end
162
+
163
+ helpers.sort_by {|name, source_files| name }
164
+ end
165
+
166
+ def compile_helpers(helper_dir)
167
+ compiler = CompileHelper.new(
168
+ :force => force,
169
+ :quiet => true
170
+ )
171
+
172
+ helpers = glob_helpers(helper_dir)
173
+ helpers.each do |(name, sources)|
174
+ compiler.process(name, *sources)
175
+ end
176
+
177
+ $LOAD_PATH.unshift compiler.output_dir
178
+ end
179
+ end
180
+ end
181
+ end