teapot 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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