bake 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +16 -0
- data/CONCEPTS +52 -0
- data/TUTORIAL +12 -23
- data/lib/bake.rb +7 -34
- data/lib/bake/addon.rb +20 -0
- data/lib/bake/context.rb +40 -60
- data/lib/bake/file_target.rb +14 -0
- data/lib/bake/plugin.rb +72 -0
- data/lib/bake/plugins/cpp.rb +122 -0
- data/lib/bake/plugins/cpp/darwin.rb +27 -0
- data/lib/bake/plugins/cpp/gcc.rb +9 -0
- data/lib/bake/plugins/cpp/gcc_toolset_base.rb +118 -0
- data/lib/bake/plugins/cpp/msvc.rb +124 -0
- data/lib/bake/plugins/cpp/qt.rb +50 -0
- data/lib/bake/plugins/cpp/toolset_base.rb +98 -0
- data/lib/bake/plugins/macro.rb +18 -0
- data/lib/bake/plugins/system.rb +46 -0
- data/lib/bake/project.rb +50 -0
- data/lib/bake/project_loader.rb +11 -15
- data/lib/bake/string_utils.rb +4 -0
- data/lib/bake/target.rb +43 -80
- data/lib/bake/toolset.rb +13 -6
- data/lib/bake_version.rb +1 -1
- metadata +19 -7
- data/lib/bake/common_scheme.rb +0 -42
- data/lib/bake/cpp_scheme.rb +0 -460
- data/lib/bake/qt_scheme.rb +0 -62
- data/lib/bake/scheme.rb +0 -104
- data/lib/bake/scheme_loader.rb +0 -28
data/CHANGELOG
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
= Bake Changelog
|
2
|
+
|
3
|
+
== Version 0.1.1
|
4
|
+
|
5
|
+
* Created a plugin infrastructure whereby missing constants now result in a search of the plugins/ directory
|
6
|
+
* Implemented Addon module which enables adding commands to bakefile contexts via the import method
|
7
|
+
* Removed scheme concept in favour of addon plugin classes so all concre Toolset subclasses (e.g. Cpp::Gcc) are now addon plugins
|
8
|
+
* Context#macro and Context#glob were moved to addon classes named Macro and System respectively which are plugins that are automatically imported into bakefile contexts
|
9
|
+
* Targets no longer found by moving up the target tree, instead all paths are relative to the current project, that is, the project owning the current target
|
10
|
+
* Added some content to the CONCEPTS document
|
11
|
+
* Introduced Context#using method which is similar to import, except that commands are looked up from addons stored in the :addons property of the current target
|
12
|
+
* Removed Cpp::Library#src and Cpp::Executable#src in favor of listing file dependencies in the dep list
|
13
|
+
* Added FileTarget class that represents simple file dependencies
|
14
|
+
* Created Target#add_dep method that can be overridden to manage individual dependency additions (for example to insert a Cpp::Object between a FileTarget and main Cpp target)
|
15
|
+
* Child targets are no longer considered dependencies and so are not built automatically when their parents are built
|
16
|
+
|
data/CONCEPTS
CHANGED
@@ -1,2 +1,54 @@
|
|
1
1
|
= Concepts
|
2
2
|
|
3
|
+
== Bake's Philosophy
|
4
|
+
|
5
|
+
Bake is a Ruby based build system similar to Rake[http://rake.rubyforge.org] and Rant[http://rant.rubyforge.org]. Unlike either of these systems, however, there is a logical separation between the the components that are being built and the methods used to build them. This separation is important for large projects having many logical components such as executables and libraries, and also supporting many compilers or platforms.
|
6
|
+
|
7
|
+
In the good old days when Make was king, adding another supported platform to a project was usually a traumatic experience. The chief reason for the difficulty was almost always that the declaration of a component and the actions executed to build that component were tightly coupled. So the structure of the project was highly dependent on the content of the actions that build it.
|
8
|
+
|
9
|
+
The pitfalls of Make are readily apparent with C++ projects that support Microsoft Visual Studio and GCC. Simple projects are easy enough to accomodate in Make, but as soon as you need to support dynamic libraries and unit test runners and platform dependent build parameters you can quickly find the number of lines in your Make code outpacing the code for your project!
|
10
|
+
|
11
|
+
The fundamental reason for this is that your project structure and the actions that build your project are orthogonal concepts. Make forces you to intertwine these two concepts causing your Makefiles to balloon in size. Bake, on the other hand, disentangles the target and toolset concepts and allows you to grow the set of targets and the toolsets that build them independently. This is the primary goal of the Bake project.
|
12
|
+
|
13
|
+
Another goal is to provide users with a large set of pre-written toolsets that will perform the actions required to turn your project into built products. Thus, in most cases, the end user simply defines the structure of the their project, indicates which toolsets to use and Bake does the rest.
|
14
|
+
|
15
|
+
The end result is that Bake requires a bare minimum of code to provide all the instructions that are needed to build a project. Furthermore, the bulk of the code defines the structure of the project rather than the behaviour of the build tools involved, so the definition files also act as a guide to the project layout, instead of being a distracting maze of complex build actions.
|
16
|
+
|
17
|
+
== Targets
|
18
|
+
|
19
|
+
In Bake, projects are broken up into components called *targets*. Targets are the basic unit of a project and can represent C libraries, zip files, Java classes or anything else that belongs in a project.
|
20
|
+
|
21
|
+
Targets are tree-like. Each target has a single parent target and can have any number of child targets. Every target that belongs in a particular project is connected in a single tree.
|
22
|
+
|
23
|
+
=== Products
|
24
|
+
|
25
|
+
Every target can have a number of *products*. The products of a target are the concrete set of files that are created during the build process.
|
26
|
+
|
27
|
+
=== Dependencies
|
28
|
+
|
29
|
+
Targets are related to each other by two relations. The first is the parent-child relation that makes up the tree structure of a project. The other is the *dependency* relation.
|
30
|
+
|
31
|
+
Before a particular target is built, there are zero or more dependencies that must be built first. If any of the dependencies fails to build, the target will fail to be built.
|
32
|
+
|
33
|
+
Dependencies have two roles for a target. First, partially determine the order in which targets will be built as outlined above. Second, their products are used by dependent target to build its products. For example, an executable target may depend on a the functionality available in a library and thus it must link against that library.
|
34
|
+
|
35
|
+
=== Options
|
36
|
+
|
37
|
+
=== Requirements
|
38
|
+
|
39
|
+
== Bakefiles
|
40
|
+
|
41
|
+
=== Commands
|
42
|
+
|
43
|
+
=== Projects
|
44
|
+
|
45
|
+
== Toolsets
|
46
|
+
|
47
|
+
== Features
|
48
|
+
|
49
|
+
=== Macros
|
50
|
+
|
51
|
+
=== Plugins
|
52
|
+
|
53
|
+
=== Addins
|
54
|
+
|
data/TUTORIAL
CHANGED
@@ -8,7 +8,7 @@ Once you've installed Bake, you should be able to execute it by running the <tt>
|
|
8
8
|
|
9
9
|
# root.bake
|
10
10
|
exe 'test' do
|
11
|
-
|
11
|
+
dep 'main.cpp'
|
12
12
|
end
|
13
13
|
|
14
14
|
The <tt>exe</tt> command specifies that we'd like to create an executable <b>target</b>. A target is any product that gets created as a result of the build process. There are many kinds of targets, such as <tt>lib</tt>s, or C++ libraries, etc. To make this example a little more concrete, let's also create <i>main.cpp</i>:
|
@@ -34,7 +34,7 @@ If you run <tt>bake</tt> from the <i>test/</i> directory you should see output r
|
|
34
34
|
Generally, it is a good idea to keep your project definitions separate from which toolset you're using since your choice of toolset is conceptually distinct from your project layout. This is especially true from cross-platform projects. For this reason, in this project we'll keep this information in a separate bakefile called <i>config.bake</i> which will also live in the <i>test/</i> directory:
|
35
35
|
|
36
36
|
# config.bake
|
37
|
-
using
|
37
|
+
using Cpp::Gcc
|
38
38
|
|
39
39
|
The <tt>using</tt> directive above tells Bake that when building C++ targets, it should use the <tt>gcc</tt> toolset. If you now run the <tt>bake</tt> command, and as long as your toolset is properly configured, you should get an executable file called <i>test</i> (or <i>test.exe</i> on Windows platforms) in the <i>test/</i> directory.
|
40
40
|
|
@@ -46,9 +46,7 @@ Unfortunately, the <i>test</i> executable will probably be accompanied by a numb
|
|
46
46
|
|
47
47
|
exe 'test' do
|
48
48
|
opt :outdir => 'bin'
|
49
|
-
|
50
|
-
src 'main.cpp'
|
51
|
-
|
49
|
+
dep 'main.cpp'
|
52
50
|
end
|
53
51
|
|
54
52
|
Note that property names are <b>symbols</b> preceded by a colon (:) whereas, in this case, the property value is a <b>string</b>. Property values can be any of a number of different types such as strings, booleans, numbers, etc.
|
@@ -57,9 +55,7 @@ Delete all the product files generated by the last build so that your directory
|
|
57
55
|
|
58
56
|
test/
|
59
57
|
config.bake
|
60
|
-
|
61
58
|
root.bake
|
62
|
-
|
63
59
|
main.cpp
|
64
60
|
|
65
61
|
Now run Bake again. Note that a new directory called <i>bin/</i> has been created and that all the products of the build have been placed inside. C++ executable targets have a number of other properties that affect different aspects of the build. For example, to turn off multithreading for a single threaded application, you would specify "<tt>opt :multithreaded? => false</tt>". Note that we've used a boolean value type instead of a string. The type is important as a value of <tt>'false'</tt> would result in the opposite behavior we would expect (this is because strings always evaluate to true when examined in a boolean context). For a full list of target properties and their types, see the Reference.
|
@@ -83,38 +79,31 @@ As a first try at managing this project, let's create a <i>root.bake</i> in the
|
|
83
79
|
|
84
80
|
# root.bake
|
85
81
|
lib 'test_lib' do
|
86
|
-
|
82
|
+
dep 'test_lib/source1.cpp', 'test_lib/source2.cpp'
|
87
83
|
end
|
88
84
|
|
89
85
|
exe 'test_exe' do
|
90
|
-
dep 'test_lib'
|
91
|
-
src 'test_exe/source1.cpp'
|
86
|
+
dep '.:test_lib', 'test_exe/source1.cpp'
|
92
87
|
end
|
93
88
|
|
94
89
|
Here we have created a <tt>lib</tt> target called <tt>test_lib</tt> containing two source files, and an executable file that depends on the library and another source file. This is pretty simple and will probably seem to work, however, there's an insidious bug waiting to cause trouble when you least expect it. Note that there are two files called <i>source1.cpp</i>. In each case, a C++ object file called <i>source1.o</i> (or <i>source1.obj</i> on Windows) will be created in the <i>test2/</i> directory, the object file from <tt>test_exe</tt> will overwrite the object file from <tt>test_lib</tt>. This will not immediately cause a problem, since the library object file was added to the library before the executable object file was created but it will almost certainly cause problems later on.
|
95
90
|
|
91
|
+
Note also the funny way we've referenced the <tt>test_lib</tt> library. Every target reference is composed of two parts: the path to the target's project and the name of the target, separated by a colon. Since the <tt>test_lib</tt> library is in the same directory as the executable the path part of the reference is simply a '.'.
|
92
|
+
|
96
93
|
=== More About Properties
|
97
94
|
|
98
|
-
The easiest way to avoid conflicts
|
95
|
+
The easiest way to avoid conflicts where output files overwrite eachother is to set the <tt>:outdir</tt> property differently on each target so that the object files will live in different directories. To do this, however, we're going to take a shortcut and only set the output directory once:
|
99
96
|
|
100
97
|
# root.bake
|
101
98
|
opt :outdir => 'bin/${name}'
|
102
|
-
|
103
|
-
|
104
99
|
lib 'test_lib' do
|
105
|
-
|
106
|
-
|
107
|
-
|
100
|
+
opt :name => 'test_lib'
|
101
|
+
dep 'test_lib/source1.cpp', 'test_lib/source2.cpp'
|
108
102
|
end
|
109
103
|
|
110
|
-
|
111
|
-
|
112
104
|
exe 'test_exe' do
|
113
|
-
|
114
|
-
dep 'test_lib'
|
115
|
-
|
116
|
-
src 'test_exe/source1.cpp'
|
117
|
-
|
105
|
+
opt :name => 'test_exe'
|
106
|
+
dep '.:test_lib', 'test_exe/source1.cpp'
|
118
107
|
end
|
119
108
|
|
120
109
|
If you run Bake now, the outputs from <tt>test_lib</tt> will end up in <i>test2/bin/test_lib/</i> and the outputs from <tt>test_exe</tt> will end up in <i>test2/bin/test_exe</i>. If you're confused, don't worry, we've taken two steps forward here. Not only is the value of the <tt>:outdir</tt> property not explicitly set on either target, but it's out in no-man's land, what gives? Furthermore, what's up with the <tt>${projname}</tt> business?
|
data/lib/bake.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
-
require 'bake/scheme_loader'
|
2
1
|
require 'bake/context'
|
3
2
|
require 'bake/project_loader'
|
3
|
+
require 'bake/string_utils'
|
4
|
+
require 'bake/plugin'
|
4
5
|
require 'bake_version'
|
5
6
|
require 'optparse'
|
6
7
|
|
@@ -21,8 +22,8 @@ module Bake
|
|
21
22
|
def initialize
|
22
23
|
raise "App instance already created" if @@instance
|
23
24
|
@@instance = self
|
24
|
-
@
|
25
|
-
@context
|
25
|
+
@context = Context.new
|
26
|
+
@context.import(Plugins::System, Plugins::Macro)
|
26
27
|
end
|
27
28
|
|
28
29
|
def run(*args)
|
@@ -42,41 +43,13 @@ module Bake
|
|
42
43
|
|
43
44
|
method = options[:clean] ? :clean : :build
|
44
45
|
if targets.empty?
|
45
|
-
|
46
|
+
@project_loader.invok_project.send(method)
|
46
47
|
else
|
47
|
-
targets.each { |target| send(method
|
48
|
+
targets.each { |target| target.send(method) }
|
48
49
|
end
|
49
50
|
end
|
50
51
|
|
51
52
|
private
|
52
|
-
def build(target)
|
53
|
-
return if target[:built?]
|
54
|
-
target.deps.each { |dep| build(dep) }
|
55
|
-
target.children.each { |child| build(child) }
|
56
|
-
toolset = toolset(target)
|
57
|
-
Dir.chdir(target[:cwdir]) do
|
58
|
-
toolset.build(target) if toolset
|
59
|
-
end
|
60
|
-
target.opt :built? => true
|
61
|
-
end
|
62
|
-
|
63
|
-
def clean(target)
|
64
|
-
target.children.each do |child|
|
65
|
-
clean(child)
|
66
|
-
end
|
67
|
-
toolset = toolset(target)
|
68
|
-
toolset.clean(target) if toolset
|
69
|
-
end
|
70
|
-
|
71
|
-
def toolset(target)
|
72
|
-
if !target.class.const_defined?(:SCHEME)
|
73
|
-
raise "no toolset for target '#{target.class.name}'"
|
74
|
-
end
|
75
|
-
scheme = target.class.const_get(:SCHEME)
|
76
|
-
return scheme.toolset if scheme
|
77
|
-
return nil
|
78
|
-
end
|
79
|
-
|
80
53
|
def determine_platform
|
81
54
|
if RUBY_PLATFORM =~ /darwin/i
|
82
55
|
return 'osx'
|
@@ -134,7 +107,7 @@ def parse_backtrace(backtrace)
|
|
134
107
|
if backtrace.find { |entry| entry =~ /^.*?(config|root|sub)\.bake:\d+/ }
|
135
108
|
return $&
|
136
109
|
end
|
137
|
-
return
|
110
|
+
return backtrace.join("\n")
|
138
111
|
end
|
139
112
|
|
140
113
|
exit_code = 1
|
data/lib/bake/addon.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bake
|
2
|
+
module Addon
|
3
|
+
def command(name, method = nil, &block)
|
4
|
+
if !block
|
5
|
+
method ||= name
|
6
|
+
addon = self
|
7
|
+
block = Proc.new do |*args|
|
8
|
+
block = args.last.is_a?(Proc) ? args.pop : nil
|
9
|
+
addon.send(method, self, *args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
commands[name] = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def commands
|
16
|
+
return @commands ||= { }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
data/lib/bake/context.rb
CHANGED
@@ -1,10 +1,33 @@
|
|
1
1
|
require 'bake/string_utils.rb'
|
2
|
+
require 'bake/addon.rb'
|
2
3
|
|
3
4
|
module Bake
|
4
5
|
class Context
|
5
|
-
|
6
|
-
|
6
|
+
attr_reader :current
|
7
|
+
|
8
|
+
def initialize
|
7
9
|
@current = nil
|
10
|
+
@commands = { }
|
11
|
+
end
|
12
|
+
|
13
|
+
def import(*addon_classes)
|
14
|
+
addon_classes.each do |addon_class|
|
15
|
+
addon = addon_class.new
|
16
|
+
addon.commands.each_pair do |name, block|
|
17
|
+
register(name, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def using(*addon_classes)
|
23
|
+
addon_classes.each do |addon_class|
|
24
|
+
addon = addon_class.new
|
25
|
+
@current.opt(:addons => addon_class.new)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def register(name, &block)
|
30
|
+
@commands[name] = block
|
8
31
|
end
|
9
32
|
|
10
33
|
def context_eval(target, str = nil, file = nil, &block)
|
@@ -21,78 +44,35 @@ module Bake
|
|
21
44
|
end
|
22
45
|
end
|
23
46
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
end
|
31
|
-
|
32
|
-
def glob(*args)
|
33
|
-
has_options = args.last.respond_to?(:to_hash)
|
34
|
-
options = has_options ? args.pop.to_hash : {}
|
35
|
-
exclude = array_opt(options, :exclude)
|
36
|
-
files = []
|
37
|
-
args.each do |pat|
|
38
|
-
matches = Dir[pat]
|
39
|
-
matches.each do |file|
|
40
|
-
if !exclude.find { |exc| File.fnmatch(exc, file) }
|
41
|
-
files.push(file)
|
42
|
-
end
|
47
|
+
def method_missing(name, *args, &block)
|
48
|
+
command = @commands[name]
|
49
|
+
return call(command, *args, &block) if command
|
50
|
+
if @current
|
51
|
+
if @current.respond_to?(name)
|
52
|
+
return @current.send(name, *args, &block)
|
43
53
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
yield(files[i])
|
54
|
+
@current.get(:addons).each do |addon|
|
55
|
+
command = addon.commands[name]
|
56
|
+
return call(command, *args, &block) if command
|
48
57
|
end
|
49
58
|
end
|
50
|
-
|
51
|
-
end
|
52
|
-
|
53
|
-
def macro(name, &block)
|
54
|
-
raise "no block given for macro '#{name}'" if !block
|
55
|
-
@current.opt(:macros => { :name => name, :block => block })
|
56
|
-
end
|
57
|
-
|
58
|
-
def method_missing(method, *args, &block)
|
59
|
-
@scheme_loader.schemes.each do |name, scheme|
|
60
|
-
if scheme.has_constructor?(method)
|
61
|
-
target = scheme.construct(method, @current, *args)
|
62
|
-
context_eval(target, &block) if block
|
63
|
-
return target
|
64
|
-
end
|
65
|
-
end
|
66
|
-
return object_method_missing(method, *args, &block) if !@current
|
67
|
-
macro = @current[:macros].find { |macro| macro[:name] == method }
|
68
|
-
if macro
|
69
|
-
return instance_exec(*args, ¯o[:block])
|
70
|
-
end
|
71
|
-
if @current.respond_to?(method)
|
72
|
-
return @current.send(method, *args, &block)
|
73
|
-
end
|
74
|
-
raise "undefined symbol '#{method}'"
|
59
|
+
raise "unknown command '#{name}'"
|
75
60
|
end
|
76
61
|
|
77
62
|
private
|
78
|
-
def
|
79
|
-
|
80
|
-
return
|
81
|
-
if opt.respond_to?(:to_ary)
|
82
|
-
opt = opt.to_ary
|
83
|
-
else
|
84
|
-
opt = [ opt ]
|
85
|
-
end
|
63
|
+
def call(command, *args, &block)
|
64
|
+
args << block if block
|
65
|
+
return instance_exec(*args, &command)
|
86
66
|
end
|
87
67
|
|
88
68
|
# took this implementation from http://tinyurl.com/gzrtn
|
89
69
|
def instance_exec(*args, &block)
|
90
70
|
mname = "__instance_exec_#{Thread.current.object_id.abs}"
|
91
|
-
class << self; self end.class_eval{ define_method(mname, &block) }
|
71
|
+
class << self; self end.class_eval { define_method(mname, &block) }
|
92
72
|
begin
|
93
73
|
ret = send(mname, *args)
|
94
74
|
ensure
|
95
|
-
class << self; self end.class_eval{ undef_method(mname) } rescue nil
|
75
|
+
class << self; self end.class_eval { undef_method(mname) } rescue nil
|
96
76
|
end
|
97
77
|
ret
|
98
78
|
end
|
data/lib/bake/plugin.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'bake/string_utils'
|
2
|
+
|
3
|
+
class Object
|
4
|
+
def self.const_missing(name)
|
5
|
+
return Bake::Plugins.const_get(name)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Bake
|
10
|
+
class PluginManager
|
11
|
+
@@plugins = { }
|
12
|
+
@@search_dirs = [ File.join(File.dirname(__FILE__), '..') ]
|
13
|
+
|
14
|
+
def self.search_dirs
|
15
|
+
return @@search_dirs
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.plugins
|
19
|
+
return @@plugins
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.load_plugin(full_name)
|
23
|
+
parent = Plugins
|
24
|
+
plugin = nil
|
25
|
+
full_name.to_s.split('::').each do |name|
|
26
|
+
plugin = load_child_plugin(parent, name)
|
27
|
+
end
|
28
|
+
return plugin
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.load_child_plugin(parent, name)
|
32
|
+
name = name.to_s
|
33
|
+
full_name = parent.name + '::' + name
|
34
|
+
plugin_file = full_name.underscore.gsub('::', '/')
|
35
|
+
|
36
|
+
plugin = nil
|
37
|
+
if parent.const_defined?(name)
|
38
|
+
plugin = parent.const_get(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
@@search_dirs.each do |rootdir|
|
42
|
+
if File.exists?(File.join(rootdir, plugin_file + '.rb'))
|
43
|
+
require plugin_file
|
44
|
+
if !plugin && parent.const_defined?(name)
|
45
|
+
plugin = parent.const_get(name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
if File.directory?(File.join(rootdir, plugin_file))
|
50
|
+
if !plugin
|
51
|
+
plugin = Module.new
|
52
|
+
parent.const_set(name, plugin)
|
53
|
+
end
|
54
|
+
def plugin.const_missing(name)
|
55
|
+
return PluginManager.load_child_plugin(self, name)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
raise "could not find plugin '#{name}'" if !plugin
|
61
|
+
@@plugins[full_name] = plugin
|
62
|
+
return plugin
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module Plugins
|
67
|
+
def self.const_missing(name)
|
68
|
+
return PluginManager.load_plugin(name)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|