halite 1.0.0.rc.1

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