teapot 0.6.0 → 0.7.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.
@@ -18,73 +18,52 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'pathname'
22
- require 'rainbow'
23
-
24
- require 'teapot/target'
25
- require 'teapot/build'
21
+ require 'teapot/loader'
22
+ require 'teapot/package'
26
23
 
27
24
  module Teapot
28
- LOADER_VERSION = "0.6"
29
- MINIMUM_LOADER_VERSION = "0.6"
30
-
31
- class IncompatibleTeapot < StandardError
32
- end
33
-
34
- class Loader
35
- # Provides install_directory and install_external methods
36
- include Build::Helpers
37
-
38
- def initialize(context, package)
39
- @context = context
40
- @package = package
41
-
42
- @defined = []
43
- @version = nil
44
- end
45
-
46
- attr :package
47
- attr :defined
48
- attr :version
49
-
50
- def required_version(version)
51
- if version >= MINIMUM_LOADER_VERSION && version <= LOADER_VERSION
52
- @version = version
53
- else
54
- raise IncompatibleTeapot.new("Version #{version} isn't compatible with current loader!\n" \
55
- "Minimum supported version: #{MINIMUM_LOADER_VERSION}; Current version: #{LOADER_VERSION}.")
56
- end
57
- end
58
-
59
- def define_target(*args, &block)
60
- target = Target.new(@context, @package, *args)
25
+ TEAPOT_FILE = "teapot.rb"
26
+ DEFAULT_CONFIGURATION_NAME = 'default'
61
27
 
62
- yield target
28
+ class AlreadyDefinedError < StandardError
29
+ def initialize(definition, previous)
30
+ super "Definition #{definition.name} in #{definition.path} has already been defined in #{previous.path}!"
31
+ end
63
32
 
64
- @context.targets[target.name] = target
33
+ def self.check(definition, definitions)
34
+ previous = definitions[definition.name]
65
35
 
66
- @defined << target
67
- end
68
-
69
- def load(path)
70
- self.instance_eval(File.read(path), path)
36
+ raise new(definition, previous) if previous
71
37
  end
72
38
  end
73
-
74
- class Context
75
- def initialize(config)
76
- @config = config
77
39
 
78
- @selection = nil
40
+ class Context
41
+ def initialize(root, options = {})
42
+ @root = Pathname(root)
43
+ @options = options
79
44
 
80
- @targets = {config.name => config}
45
+ @targets = {}
46
+ @generators = {}
47
+ @configurations = {}
81
48
 
82
49
  @dependencies = []
83
50
  @selection = Set.new
51
+
52
+ @loaded = {}
53
+
54
+ # Load the root package:
55
+ defined = load(root_package)
56
+
57
+ # Find the default configuration, if it exists:
58
+ @default_configuration = defined.default_configuration
84
59
  end
85
60
 
86
- attr :config
61
+ attr :root
62
+ attr :options
63
+
87
64
  attr :targets
65
+ attr :generators
66
+ attr :configurations
88
67
 
89
68
  def select(names)
90
69
  names.each do |name|
@@ -95,27 +74,90 @@ module Teapot
95
74
  end
96
75
  end
97
76
  end
98
-
77
+
99
78
  attr :dependencies
100
79
  attr :selection
101
-
80
+
102
81
  def direct_targets(ordered)
103
82
  @dependencies.collect do |dependency|
104
83
  ordered.find{|(package, _)| package.provides? dependency}
105
84
  end.compact
106
85
  end
107
-
86
+
87
+ def << definition
88
+ case definition
89
+ when Target
90
+ AlreadyDefinedError.check(definition, @targets)
91
+
92
+ @targets[definition.name] = definition
93
+ when Generator
94
+ AlreadyDefinedError.check(definition, @generators)
95
+
96
+ @generators[definition.name] = definition
97
+ when Configuration
98
+ if definition.public?
99
+ # The root package implicitly defines the default configuration.
100
+ if definition.name == DEFAULT_CONFIGURATION_NAME
101
+ raise AlreadyDefinedError.new(definition, root_package)
102
+ end
103
+
104
+ AlreadyDefinedError.check(definition, @configurations)
105
+
106
+ @configurations[definition.name] = definition
107
+ end
108
+ end
109
+ end
110
+
108
111
  def load(package)
109
- loader = Loader.new(self, package)
112
+ # In certain cases, a package record might be loaded twice. This typically occurs when multiple configurations are loaded in the same context, or if a package has already been loaded (as is typical with the root package).
113
+ @loaded.fetch(package) do
114
+ loader = Loader.new(self, package)
115
+
116
+ loader.load(TEAPOT_FILE)
117
+
118
+ # Load the definitions into the current context:
119
+ loader.defined.each do |definition|
120
+ self << definition
121
+ end
122
+
123
+ # Save the definitions per-package:
124
+ @loaded[package] = loader.defined
125
+ end
126
+ end
127
+
128
+ attr :default_configuration
129
+
130
+ def configuration_named(name)
131
+ if name == DEFAULT_CONFIGURATION_NAME
132
+ configuration = @default_configuration
133
+ else
134
+ configuration = @configurations[name]
135
+ end
110
136
 
111
- path = (package.path + package.loader_path).to_s
112
- loader.load(path)
137
+ if configuration
138
+ configuration.materialize
139
+ end
140
+ end
141
+
142
+ def unresolved(packages)
143
+ failed_to_load = Set.new
113
144
 
114
- if loader.version == nil
115
- raise IncompatibleTeapot.new("No version specified in #{path}!")
145
+ packages.collect do |package|
146
+ begin
147
+ definitions = load(package)
148
+ rescue NonexistantTeapotError
149
+ failed_to_load << package
150
+ end
116
151
  end
117
152
 
118
- loader.defined
153
+ return failed_to_load
154
+ end
155
+
156
+ private
157
+
158
+ # The root package is a special package which is used to load definitions from a given root path.
159
+ def root_package
160
+ @root_package ||= Package.new(@root, "root")
119
161
  end
120
162
  end
121
163
  end
@@ -0,0 +1,56 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'teapot/configuration'
22
+ require 'teapot/version'
23
+
24
+ require 'uri'
25
+ require 'rainbow'
26
+ require 'fileutils'
27
+
28
+ module Teapot
29
+ class Controller
30
+ MAXIMUM_FETCH_DEPTH = 20
31
+
32
+ def initialize(root, options)
33
+ @root = Pathname(root)
34
+ @options = options
35
+
36
+ @log_output = @options.fetch(:log, $stdout)
37
+
38
+ @options[:maximum_fetch_depth] ||= MAXIMUM_FETCH_DEPTH
39
+ end
40
+
41
+ def log(*args)
42
+ @log_output.puts *args
43
+ end
44
+
45
+ private
46
+
47
+ def load_teapot
48
+ configuration_name = @options[:configuration] || DEFAULT_CONFIGURATION_NAME
49
+
50
+ context = Context.new(@root)
51
+ configuration = context.configuration_named(configuration_name)
52
+
53
+ return context, configuration
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,71 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'teapot/controller'
22
+
23
+ module Teapot
24
+ class Controller
25
+ def build(package_names)
26
+ context, configuration = load_teapot
27
+
28
+ configuration.load_all
29
+
30
+ context.select(package_names)
31
+
32
+ chain = Dependency::chain(context.selection, context.dependencies, context.targets.values)
33
+
34
+ if chain.unresolved.size > 0
35
+ log "Unresolved dependencies:"
36
+
37
+ chain.unresolved.each do |(name, parent)|
38
+ log "#{parent} depends on #{name.inspect}".color(:red)
39
+
40
+ conflicts = chain.conflicts[name]
41
+
42
+ if conflicts
43
+ conflicts.each do |conflict|
44
+ log " - provided by #{conflict.inspect}".color(:red)
45
+ end
46
+ end
47
+ end
48
+
49
+ abort "Cannot continue build due to unresolved dependencies!".color(:red)
50
+ end
51
+
52
+ log "Resolved: #{chain.resolved.inspect}".color(:magenta)
53
+
54
+ ordered = chain.ordered
55
+
56
+ if @options[:only]
57
+ ordered = context.direct_targets(ordered)
58
+ end
59
+
60
+ ordered.each do |(target, dependency)|
61
+ log "Building #{target.name} for dependency #{dependency}...".color(:cyan)
62
+
63
+ if target.respond_to?(:build!) and !@options[:dry]
64
+ target.build!(configuration)
65
+ end
66
+ end
67
+
68
+ log "Completed build successfully.".color(:green)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'teapot/controller'
22
+
23
+ module Teapot
24
+ class Controller
25
+ def clean
26
+ context, configuration = load_teapot
27
+
28
+ log "Removing #{configuration.platforms_path}...".color(:cyan)
29
+ FileUtils.rm_rf configuration.platforms_path
30
+
31
+ log "Removing #{configuration.packages_path}...".color(:cyan)
32
+ FileUtils.rm_rf configuration.packages_path
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'teapot/controller'
22
+ require 'teapot/controller/fetch'
23
+
24
+ require 'teapot/name'
25
+
26
+ module Teapot
27
+ class Controller
28
+ def create(project_name, source, packages)
29
+ name = Name.new(project_name)
30
+
31
+ log "Creating project named #{project_name} at path #{@root}...".color(:cyan)
32
+
33
+ File.open(@root + TEAPOT_FILE, "w") do |output|
34
+ output.puts "\# Teapot configuration generated at #{Time.now.to_s}", ''
35
+
36
+ output.puts "required_version #{VERSION.dump}", ''
37
+
38
+ output.puts "define_configuration #{name.target.dump} do |configuration|"
39
+
40
+ output.puts "\tconfiguration[:source] = #{source.dump}", ''
41
+
42
+ packages.each do |name|
43
+ output.puts "\tconfiguration.import! #{name.dump}"
44
+ end
45
+
46
+ output.puts "end"
47
+ end
48
+
49
+ fetch
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,167 @@
1
+ # Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'teapot/controller'
22
+
23
+ module Teapot
24
+ class Controller
25
+ def fetch
26
+ context, configuration = load_teapot
27
+
28
+ resolved = Set.new
29
+ unresolved = context.unresolved(configuration.packages)
30
+ tries = 0
31
+
32
+ while tries < @options[:maximum_fetch_depth]
33
+ configuration.packages.each do |package|
34
+ next if resolved.include? package
35
+
36
+ fetch_package(context, configuration, package)
37
+
38
+ # We are done with this package, don't try to process it again:
39
+ resolved << package
40
+ end
41
+
42
+ # Resolve any/all imports:
43
+ configuration = configuration.materialize
44
+
45
+ previously_unresolved = unresolved
46
+ unresolved = context.unresolved(configuration.packages)
47
+
48
+ # No additional packages were resolved, we have reached a fixed point:
49
+ if previously_unresolved == unresolved || unresolved.count == 0
50
+ break
51
+ end
52
+
53
+ tries += 1
54
+ end
55
+
56
+ if unresolved.count > 0
57
+ log "Could not fetch all packages!".color(:red)
58
+ unresolved.each do |package|
59
+ log "\t#{package}".color(:red)
60
+ end
61
+ else
62
+ log "Completed fetch successfully.".color(:green)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def current_commit(package)
69
+ IO.popen(['git', '--git-dir', (package.path + ".git").to_s, 'rev-parse', '--verify', 'HEAD']) do |io|
70
+ io.read.chomp!
71
+ end
72
+ end
73
+
74
+ def fetch_package(context, configuration, package)
75
+ destination_path = package.path
76
+ lock_store = configuration.lock_store
77
+
78
+ if package.local?
79
+ log "Linking local #{package}...".color(:cyan)
80
+
81
+ local_path = context.root + package.options[:local]
82
+
83
+ # Make the top level directory if required:
84
+ destination_path.dirname.mkpath
85
+
86
+ unless destination_path.exist?
87
+ destination_path.make_symlink(local_path)
88
+ end
89
+ elsif package.external?
90
+ package_lock = lock_store.transaction(true){|store| store[package.name]}
91
+
92
+ log "Fetching #{package}...".color(:cyan)
93
+
94
+ base_uri = URI(package.options[:source].to_s)
95
+
96
+ if base_uri.scheme == nil || base_uri.scheme == 'file'
97
+ base_uri = URI "file://" + File.expand_path(base_uri.path, context.root) + "/"
98
+ end
99
+
100
+ branch = package.options.fetch(:version, 'master')
101
+
102
+ if package_lock
103
+ log "Package locked to commit: #{package_lock[:branch]}/#{package_lock[:commit]}"
104
+
105
+ branch = package_lock[:branch]
106
+ end
107
+
108
+ unless destination_path.exist?
109
+ log "Cloning package at path #{destination_path} ...".color(:cyan)
110
+
111
+ begin
112
+ destination_path.mkpath
113
+
114
+ external_url = package.external_url(context.root)
115
+
116
+ Commands.run("git", "clone", external_url, destination_path, "--branch", branch)
117
+
118
+ # Checkout the specific version if it was given:
119
+ if package_lock
120
+ Commands.run("git", "reset", "--hard", package_lock[:commit])
121
+ end
122
+
123
+ Dir.chdir(destination_path) do
124
+ Commands.run("git", "submodule", "update", "--init", "--recursive")
125
+ end
126
+ rescue
127
+ log "Removing incomplete package at path #{destination_path}...".color(:red)
128
+
129
+ # Clean up if the git checkout process is interrupted:
130
+ destination_path.rmtree
131
+
132
+ raise
133
+ end
134
+ else
135
+ log "Updating package at path #{destination_path} ...".color(:cyan)
136
+
137
+ Dir.chdir(destination_path) do
138
+ Commands.run("git", "fetch", "origin")
139
+
140
+ Commands.run("git", "checkout", branch)
141
+
142
+ # Pull any changes, if you might get the error from above:
143
+ # Your branch is behind 'origin/0.1' by 1 commit, and can be fast-forwarded.
144
+ Commands.run("git", "pull")
145
+
146
+ # Checkout the specific version if it was given:
147
+ if package_lock
148
+ Commands.run("git", "reset", "--hard", package_lock[:commit])
149
+ end
150
+
151
+ Commands.run("git", "submodule", "update", "--init", "--recursive")
152
+ end
153
+ end
154
+
155
+ # Lock the package, unless it was already locked:
156
+ unless package_lock
157
+ lock_store.transaction do |store|
158
+ store[package.name] = {
159
+ :branch => branch,
160
+ :commit => current_commit(package)
161
+ }
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end