halite 1.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +15 -0
  5. data/LICENSE +202 -0
  6. data/README.md +75 -0
  7. data/Rakefile +22 -0
  8. data/halite.gemspec +33 -0
  9. data/lib/berkshelf/halite.rb +2 -0
  10. data/lib/halite.rb +12 -0
  11. data/lib/halite/berkshelf/helper.rb +69 -0
  12. data/lib/halite/berkshelf/source.rb +56 -0
  13. data/lib/halite/converter.rb +17 -0
  14. data/lib/halite/converter/libraries.rb +40 -0
  15. data/lib/halite/converter/metadata.rb +21 -0
  16. data/lib/halite/converter/other.rb +19 -0
  17. data/lib/halite/converter/readme.rb +20 -0
  18. data/lib/halite/dependencies.rb +72 -0
  19. data/lib/halite/error.rb +4 -0
  20. data/lib/halite/gem.rb +82 -0
  21. data/lib/halite/rake_helper.rb +151 -0
  22. data/lib/halite/rake_tasks.rb +2 -0
  23. data/lib/halite/spec_helper.rb +134 -0
  24. data/lib/halite/spec_helper/empty/README.md +1 -0
  25. data/lib/halite/spec_helper/runner.rb +43 -0
  26. data/lib/halite/version.rb +3 -0
  27. data/spec/converter/libraries_spec.rb +152 -0
  28. data/spec/converter/metadata_spec.rb +60 -0
  29. data/spec/converter/other_spec.rb +56 -0
  30. data/spec/converter/readme_spec.rb +55 -0
  31. data/spec/converter_spec.rb +14 -0
  32. data/spec/data/gems/test1/Rakefile +1 -0
  33. data/spec/data/gems/test1/lib/test1.rb +2 -0
  34. data/spec/data/gems/test1/lib/test1/version.rb +3 -0
  35. data/spec/data/gems/test1/test1.gemspec +25 -0
  36. data/spec/data/gems/test2/Rakefile +1 -0
  37. data/spec/data/gems/test2/chef/attributes.rb +0 -0
  38. data/spec/data/gems/test2/chef/recipes/default.rb +0 -0
  39. data/spec/data/gems/test2/chef/templates/default/conf.erb +0 -0
  40. data/spec/data/gems/test2/lib/test2.rb +4 -0
  41. data/spec/data/gems/test2/lib/test2/resource.rb +6 -0
  42. data/spec/data/gems/test2/lib/test2/version.rb +3 -0
  43. data/spec/data/gems/test2/test2.gemspec +24 -0
  44. data/spec/data/gems/test3/Rakefile +1 -0
  45. data/spec/data/gems/test3/chef/recipes/default.rb +1 -0
  46. data/spec/data/gems/test3/lib/test3.rb +4 -0
  47. data/spec/data/gems/test3/lib/test3/dsl.rb +15 -0
  48. data/spec/data/gems/test3/lib/test3/version.rb +3 -0
  49. data/spec/data/gems/test3/test3.gemspec +24 -0
  50. data/spec/data/integration_cookbooks/test1/libraries/test1.rb +3 -0
  51. data/spec/data/integration_cookbooks/test1/libraries/test1__version.rb +4 -0
  52. data/spec/data/integration_cookbooks/test1/metadata.rb +4 -0
  53. data/spec/data/integration_cookbooks/test2/attributes.rb +0 -0
  54. data/spec/data/integration_cookbooks/test2/libraries/test2.rb +5 -0
  55. data/spec/data/integration_cookbooks/test2/libraries/test2__resource.rb +7 -0
  56. data/spec/data/integration_cookbooks/test2/libraries/test2__version.rb +4 -0
  57. data/spec/data/integration_cookbooks/test2/metadata.rb +4 -0
  58. data/spec/data/integration_cookbooks/test2/recipes/default.rb +0 -0
  59. data/spec/data/integration_cookbooks/test2/templates/default/conf.erb +0 -0
  60. data/spec/data/integration_cookbooks/test3/libraries/test3.rb +5 -0
  61. data/spec/data/integration_cookbooks/test3/libraries/test3__dsl.rb +16 -0
  62. data/spec/data/integration_cookbooks/test3/libraries/test3__version.rb +4 -0
  63. data/spec/data/integration_cookbooks/test3/metadata.rb +4 -0
  64. data/spec/data/integration_cookbooks/test3/recipes/default.rb +1 -0
  65. data/spec/dependencies_spec.rb +167 -0
  66. data/spec/gem_spec.rb +164 -0
  67. data/spec/integration_spec.rb +104 -0
  68. data/spec/spec_helper.rb +28 -0
  69. metadata +307 -0
@@ -0,0 +1,56 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'halite/gem'
18
+ require 'berkshelf/source'
19
+ require 'berkshelf/api_client/remote_cookbook'
20
+
21
+ module Halite
22
+ module Berkshelf
23
+
24
+ class Source < ::Berkshelf::Source
25
+ def initialize
26
+ super 'https://supermarket.chef.io'
27
+ end
28
+
29
+ def build_universe
30
+ # Scan all gems
31
+ ::Gem::Specification.stubs.map do |spec|
32
+ Gem.new(spec)
33
+ end.select do |cook|
34
+ cook.is_halite_cookbook?
35
+ end.map do |cook|
36
+ # Build a fake "remote" cookbook
37
+ ::Berkshelf::APIClient::RemoteCookbook.new(
38
+ cook.cookbook_name,
39
+ cook.version,
40
+ {
41
+ location_type: 'halite',
42
+ location_path: cook.name,
43
+ dependencies: cook.cookbook_dependencies.inject({}) {|memo, dep| memo[dep.name] = dep.requirement; memo },
44
+ },
45
+ )
46
+ end
47
+ end
48
+
49
+ def to_s
50
+ "Halite gems"
51
+ end
52
+ alias :uri :to_s
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,17 @@
1
+ require 'halite/converter/libraries'
2
+ require 'halite/converter/metadata'
3
+ require 'halite/converter/other'
4
+ require 'halite/converter/readme'
5
+
6
+ module Halite
7
+ module Converter
8
+
9
+ def self.write(spec, base_path)
10
+ Metadata.write(spec, base_path)
11
+ Libraries.write(spec, base_path)
12
+ Other.write(spec, base_path)
13
+ Readme.write(spec, base_path)
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ module Halite
2
+ module Converter
3
+ module Libraries
4
+
5
+ # Chef doesn't allow subfolders under libraries/ currently
6
+ def self.flatten_filename(path)
7
+ path.gsub(/\//, '__')
8
+ end
9
+
10
+ def self.generate(spec, data, entry_point=false)
11
+ # No newline on the header so that line numbers in the files aren't changed.
12
+ buf = (entry_point ? "ENV['HALITE_LOAD'] = '1'; begin; " : "if ENV['HALITE_LOAD']; ")
13
+ # Rewrite requires to require_relative as needed.
14
+ data = data.gsub(/require ['"](#{spec.name}[^'"]*)['"]/) { "require_relative '#{flatten_filename($1)}'" }
15
+ spec.cookbook_dependencies.each do |dep|
16
+ next unless dep.type == :dependencies
17
+ # This is kind of gross, but not sure what else to do
18
+ data = data.gsub(/require ['"](#{dep.name}[^'"]*)['"]/) { "require_relative '../../#{dep.name}/libraries/#{flatten_filename($1)}'" }
19
+ end
20
+ buf << data.rstrip
21
+ # Match up with the header. All files get one line longer. ¯\_(ツ)_/¯
22
+ buf << (entry_point ? "\nensure; ENV.delete('HALITE_LOAD'); end\n" : "\nend\n")
23
+ buf
24
+ end
25
+
26
+ def self.write(spec, base_path, entry_point_name=nil)
27
+ entry_point_name ||= spec.name
28
+ # Handle both cases, with .rb and without
29
+ entry_point_name += '.rb' unless entry_point_name.end_with?('.rb')
30
+ lib_path = File.join(base_path, 'libraries')
31
+ # Create cookbook's libraries folder
32
+ Dir.mkdir(lib_path) unless File.directory?(lib_path)
33
+ spec.each_library_file do |path, rel_path|
34
+ IO.write(File.join(lib_path, flatten_filename(rel_path)), generate(spec, IO.read(path), entry_point_name == rel_path))
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ module Halite
2
+ module Converter
3
+ module Metadata
4
+
5
+ def self.generate(spec)
6
+ buf = spec.license_header
7
+ buf << "name #{spec.cookbook_name.inspect}\n"
8
+ buf << "version #{spec.version.inspect}\n"
9
+ spec.cookbook_dependencies.each do |dep|
10
+ buf << "depends #{dep.name.inspect}, #{dep.requirement.inspect}\n"
11
+ end
12
+ buf
13
+ end
14
+
15
+ def self.write(spec, base_path)
16
+ IO.write(File.join(base_path, 'metadata.rb'), generate(spec))
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module Halite
2
+ module Converter
3
+ module Other
4
+
5
+ def self.write(spec, base_path)
6
+ spec.each_file('chef') do |path, rel_path|
7
+ dir_path = File.dirname(rel_path)
8
+ FileUtils.mkdir_p(File.join(base_path, dir_path)) unless dir_path == '.'
9
+ File.open(path, 'rb') do |in_f|
10
+ File.open(File.join(base_path, rel_path), 'wb') do |out_f|
11
+ IO.copy_stream(in_f, out_f)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Halite
2
+ module Converter
3
+ module Readme
4
+
5
+ def self.write(spec, base_path)
6
+ readme_path = %w{README.md README README.txt readme.md readme readme.txt}.map do |name|
7
+ File.join(spec.full_gem_path, name)
8
+ end.find {|path| File.exists?(path) }
9
+ if readme_path
10
+ File.open(readme_path, 'rb') do |in_f|
11
+ File.open(File.join(base_path, File.basename(readme_path)), 'wb') do |out_f|
12
+ IO.copy_stream(in_f, out_f)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,72 @@
1
+ require 'halite/error'
2
+
3
+ module Halite
4
+ module Dependencies
5
+ class InvalidDependencyError < Error; end
6
+
7
+ Dependency = Struct.new(:name, :requirement, :type)
8
+
9
+ def self.extract(spec)
10
+ deps = []
11
+ deps += clean_and_tag(extract_from_requirements(spec), :requirements)
12
+ deps += clean_and_tag(extract_from_metadata(spec), :metadata)
13
+ deps += clean_and_tag(extract_from_dependencies(spec), :dependencies)
14
+ deps
15
+ end
16
+
17
+ def self.extract_from_requirements(spec)
18
+ # Simple dependencies in the requirements array.
19
+ spec.requirements
20
+ end
21
+
22
+ def self.extract_from_metadata(spec)
23
+ # This will only work on Rubygems 2.0 or higher I think, gee thats just too bad.
24
+ # The metadata can only be a single string, so split on comma.
25
+ spec.metadata.fetch('halite_dependencies', '').split(/,/)
26
+ end
27
+
28
+ def self.extract_from_dependencies(spec)
29
+ # Find any gem dependencies that are cookbooks in disguise.
30
+ spec.dependencies.select do |dep|
31
+ Gem.new(dep).is_halite_cookbook?
32
+ end.map do |dep|
33
+ [Gem.new(dep).cookbook_name] + dep.requirements_list
34
+ end
35
+ end
36
+
37
+ def self.clean_and_tag(deps, tag)
38
+ deps.map do |dep|
39
+ dep = clean(dep)
40
+ Dependency.new(dep[0], dep[1], tag)
41
+ end
42
+ end
43
+
44
+
45
+ def self.clean(dep)
46
+ # Convert to an array of strings
47
+ dep = Array(dep).map {|obj| obj.to_s.strip }
48
+ # Unpack single strings like 'foo >= 1.0'
49
+ dep = dep.first.split(/\s+/, 2) if dep.length == 1
50
+ # Default version constraint to match rubygems behavior when sourcing from simple strings
51
+ dep << '>= 0' if dep.length == 1
52
+ raise InvalidDependencyError.new("Chef only supports a single version constraint on each dependency: #{dep}") if dep.length > 2 # ಠ_ಠ
53
+ dep[1] = clean_requirement(dep[1])
54
+ dep
55
+ end
56
+
57
+ def self.clean_requirement(req)
58
+ req = ::Gem::Requirement.create(req)
59
+ req.requirements[0][1] = clean_version(req.requirements[0][1])
60
+ req.to_s
61
+ end
62
+
63
+ def self.clean_version(ver)
64
+ segments = ver.segments
65
+ # Various ways Chef differs from Rubygems
66
+ raise InvalidDependencyError.new("Chef only supports two or three version segments: #{ver}") if segments.length < 1 || segments.length > 3
67
+ segments.each {|s| raise InvalidDependencyError.new("Chef does not support pre-release version numbers: #{ver}") unless s.is_a?(Integer) }
68
+ segments << 0 if segments.length == 1
69
+ ::Gem::Version.new(segments.join('.'))
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,4 @@
1
+ module Halite
2
+ class Error < ::Exception
3
+ end
4
+ end
data/lib/halite/gem.rb ADDED
@@ -0,0 +1,82 @@
1
+ require 'halite/dependencies'
2
+
3
+ module Halite
4
+ class Gem
5
+ # name can be either a string name, Gem::Dependency, or Gem::Specification
6
+ def initialize(name, version=nil)
7
+ name = name.to_spec if name.is_a?(::Gem::Dependency) # Allow passing either
8
+ if name.is_a?(::Gem::Specification)
9
+ raise Error.new("Cannot pass version when using an explicit specficiation") if version
10
+ @spec = name
11
+ @name = spec.name
12
+ else
13
+ @name = name
14
+ @version = version
15
+ raise Error.new("Gem #{name}#{version ? " v#{version}" : ''} not found") unless spec
16
+ end
17
+ end
18
+
19
+ def spec
20
+ @spec ||= ::Gem::Dependency.new(@name, ::Gem::Requirement.new(@version)).to_spec
21
+ end
22
+
23
+ def method_missing(*args)
24
+ spec.send(*args)
25
+ end
26
+
27
+ def version
28
+ spec.version.to_s
29
+ end
30
+
31
+ def cookbook_name
32
+ if spec.metadata.include?('halite_name')
33
+ spec.metadata['halite_name']
34
+ else
35
+ spec.name.gsub(/(^(chef|cookbook)[_-])|([_-](chef|cookbook))$/, '')
36
+ end
37
+ end
38
+
39
+ # The Rubygems API is shit and just assumes the file layout
40
+ def spec_file
41
+ File.join(spec.full_gem_path, spec.name + '.gemspec')
42
+ end
43
+
44
+ def license_header
45
+ IO.readlines(spec_file).take_while { |line| line.strip.empty? || line.strip.start_with?('#') }.join('')
46
+ end
47
+
48
+ def each_file(prefix_paths=nil, &block)
49
+ files = []
50
+ spec.files.each do |path|
51
+ prefix = if prefix_paths
52
+ Array(prefix_paths).map {|p| p.end_with?('/') ? p : p + '/' }.find {|p| path.start_with?(p) }
53
+ else
54
+ ''
55
+ end
56
+ next unless prefix # No match
57
+ value = [
58
+ File.join(spec.full_gem_path, path), # Full path
59
+ path[prefix.length..-1], # Relative path
60
+ ]
61
+ files << value
62
+ block.call(*value) if block
63
+ end
64
+ files.sort! # To be safe
65
+ end
66
+
67
+ # Special case of the above using spec's require paths
68
+ def each_library_file(&block)
69
+ each_file(spec.require_paths, &block)
70
+ end
71
+
72
+ def cookbook_dependencies
73
+ @cookbook_dependencies ||= Dependencies.extract(spec)
74
+ end
75
+
76
+ # Is this gem really a cookbook? (anything that depends directly on halite and doesn't have the ignore flag)
77
+ def is_halite_cookbook?
78
+ spec.dependencies.any? {|subdep| subdep.name == 'halite'} && !spec.metadata.include?('halite_ignore')
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,151 @@
1
+ # Much inspiration from Bundler's GemHelper. Thanks!
2
+ require 'tmpdir'
3
+ require 'thor/shell'
4
+
5
+ require 'halite'
6
+ require 'halite/error'
7
+
8
+ module Halite
9
+ class RakeHelper
10
+ include Rake::DSL if defined? Rake::DSL
11
+
12
+ def self.install_tasks(*args)
13
+ new(*args).install
14
+ end
15
+
16
+ attr_accessor :gem_name, :base, :cookbook_name
17
+
18
+ def initialize(gem_name=nil, base=nil, no_gem=nil, no_foodcritic=nil, no_kitchen=nil)
19
+ if gem_name.is_a?(Hash)
20
+ opts = gem_name.inject({}) {|memo, (key, value)| memo[key.to_s] = value; memo }
21
+ gem_name = opts['gem_name']
22
+ base = opts['base']
23
+ no_gem = opts['no_gem']
24
+ no_foodcritic = opts['no_foodcritic']
25
+ no_kitchen = opts['no_kitchen']
26
+ end
27
+ # Order is important, find_gem_name needs base to be set
28
+ @base = base || if defined? Rake
29
+ Rake.original_dir
30
+ else
31
+ Dir.pwd
32
+ end
33
+ @gem_name = gem_name || find_gem_name
34
+ @gemspec = Bundler.load_gemspec(@gem_name+'.gemspec')
35
+ @no_gem = no_gem
36
+ @no_foodcritic = no_foodcritic
37
+ @no_kitchen = no_kitchen
38
+ end
39
+
40
+ def find_gem_name
41
+ specs = Dir[File.join(base, '*.gemspec')]
42
+ raise Error.new("Unable to automatically determine gem name from specs in #{base}. Please set the gem name via Halite::RakeHelper.install_tasks(gem_name: 'name').") if specs.length != 1
43
+ File.basename(specs.first, '.gemspec')
44
+ end
45
+
46
+ def pkg_path
47
+ @pkg_path ||= File.join(base, 'pkg', "#{@gem_name}-#{@gemspec.version}")
48
+ end
49
+
50
+ def shell
51
+ @shell ||= if @no_color || !STDOUT.tty?
52
+ Thor::Shell::Basic
53
+ else
54
+ Thor::Base.shell
55
+ end.new
56
+ end
57
+
58
+ def install
59
+ # Core Halite tasks
60
+ desc "Convert #{@gem_name}-#{@gemspec.version} to a cookbook in the pkg directory"
61
+ task 'chef:build' do
62
+ build_cookbook
63
+ end
64
+
65
+ desc "Push #{@gem_name}-#{@gemspec.version} to Supermarket"
66
+ task 'chef:release' => ['chef:build'] do
67
+ release_cookbook
68
+ end
69
+
70
+ # Patch the core gem tasks to run ours too
71
+ if !@no_gem
72
+ task 'build' => ['chef:build']
73
+ task 'release' => ['chef:release']
74
+ end
75
+
76
+ # Foodcritic doesn't have a config file, so just always try to add it.
77
+ if !@no_foodcritic
78
+ install_foodcritic
79
+ end
80
+
81
+ # If a .kitchen.yml exists, install the Test Kitchen tasks.
82
+ if !@no_kitchen && File.exists?(File.join(@base, '.kitchen.yml'))
83
+ install_kitchen
84
+ end
85
+ end
86
+
87
+ def install_foodcritic
88
+ require 'foodcritic'
89
+
90
+ desc 'Run Foodcritic linter'
91
+ task 'chef:foodcritic' do
92
+ Dir.mktmpdir('halite_test') do |path|
93
+ Halite.convert(gem_name, path)
94
+ sh("foodcritic -f any '#{path}'")
95
+ end
96
+ end
97
+
98
+ add_test_task('chef:foodcritic')
99
+ rescue LoadError
100
+ task 'chef:foodcritic' do
101
+ raise "Foodcritic is not available. You can use Halite::RakeHelper.install_tasks(no_foodcritic: true) to disable it."
102
+ end
103
+ end
104
+
105
+ def install_kitchen
106
+ desc 'Run all Test Kitchen tests'
107
+ task 'chef:kitchen' do
108
+ sh 'kitchen test -d always'
109
+ end
110
+
111
+ add_test_task('chef:kitchen')
112
+ end
113
+
114
+ def add_test_task(name)
115
+ # Only set a description if the task doesn't already exist
116
+ desc 'Run all tests' unless Rake.application.lookup('test')
117
+ task :test => [name]
118
+ end
119
+
120
+ def build_cookbook
121
+ # Make sure pkg/name-version exists and is empty
122
+ FileUtils.mkdir_p(pkg_path)
123
+ remove_files_in_folder(pkg_path)
124
+ Halite.convert(gem_name, pkg_path)
125
+ shell.say("#{@gem_name} #{@gemspec.version} converted to pkg/#{@gem_name}-#{@gemspec.version}/.", :green)
126
+ end
127
+
128
+ def release_cookbook
129
+ Dir.chdir(pkg_path) do
130
+ #sh('stove --sign')
131
+ end
132
+ end
133
+
134
+ # Remove everything in a path, but not the directory itself
135
+ def remove_files_in_folder(base_path)
136
+ existing_files = Dir.glob(File.join(base_path, '**', '*'), File::FNM_DOTMATCH).map {|path| File.expand_path(path)}.uniq.reverse # expand_path just to normalize foo/. -> foo
137
+ existing_files.delete(base_path) # Don't remove the base
138
+ # Fuck FileUtils, it is a confusing pile of fail for remove*/rm*
139
+ existing_files.each do |path|
140
+ if File.file?(path)
141
+ File.unlink(path)
142
+ elsif File.directory?(path)
143
+ Dir.unlink(path)
144
+ else
145
+ # Because paranoia
146
+ raise Error.new("Unknown type of file at '#{path}', possible symlink deletion attack")
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end