Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(699)

Side by Side Diff: scripts/slave/recipe_loader.py

Issue 1111413005: Some changes to allow recipes and modules to live noncentrally (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Review comments Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « scripts/slave/recipe_config_types.py ('k') | scripts/slave/recipe_modules/__init__.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2013 The Chromium Authors. All rights reserved. 1 # Copyright 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import copy
6 import imp 5 import imp
7 import inspect 6 import inspect
8 import os 7 import os
9 import sys 8 import sys
10 9
11 from .recipe_util import (ROOT_PATH, RECIPE_DIRS, MODULE_DIRS, 10 from .recipe_util import (ROOT_PATH, RECIPE_DIRS, MODULE_DIRS,
12 cached_unary, scan_directory) 11 cached_unary, scan_directory)
13 from .recipe_api import RecipeApi, RecipeApiPlain 12 from .recipe_api import RecipeApi, RecipeApiPlain
14 from .recipe_config import ConfigContext 13 from .recipe_config import ConfigContext
15 from .recipe_config_types import Path, ModuleBasePath 14 from .recipe_config_types import Path, ModuleBasePath, RECIPE_MODULE_PREFIX
16 from .recipe_test_api import RecipeTestApi, DisabledTestData 15 from .recipe_test_api import RecipeTestApi, DisabledTestData
17 16
18 17
19 class NoSuchRecipe(Exception): 18 class NoSuchRecipe(Exception):
20 """Raised by load_recipe is recipe is not found.""" 19 """Raised by load_recipe is recipe is not found."""
21 20
22 21
23 class RecipeScript(object): 22 class RecipeScript(object):
24 """Holds dict of an evaluated recipe script.""" 23 """Holds dict of an evaluated recipe script."""
25 24
26 def __init__(self, recipe_dict): 25 def __init__(self, recipe_dict):
27 for k, v in recipe_dict.iteritems(): 26 for k, v in recipe_dict.iteritems():
28 setattr(self, k, v) 27 setattr(self, k, v)
29 28
30 @classmethod 29 @classmethod
31 def from_script_path(cls, script_path): 30 def from_script_path(cls, script_path, universe):
32 """Evaluates a script and returns RecipeScript instance.""" 31 """Evaluates a script and returns RecipeScript instance."""
33 script_vars = {} 32 script_vars = {}
34 execfile(script_path, script_vars) 33 execfile(script_path, script_vars)
35 script_vars['__file__'] = script_path 34 script_vars['__file__'] = script_path
35 script_vars['LOADED_DEPS'] = universe.deps_from_mixed(
36 script_vars.get('DEPS', []), os.path.basename(script_path))
36 return cls(script_vars) 37 return cls(script_vars)
37 38
38 @classmethod 39
39 def from_module_object(cls, module_obj): 40 class Dependency(object):
40 """Converts python module object into RecipeScript instance.""" 41 def load(self, universe):
41 return cls(module_obj.__dict__) 42 raise NotImplementedError()
43
44 @property
45 def local_name(self):
46 raise NotImplementedError()
47
48 @property
49 def unique_name(self):
50 """A unique identifier for the module that this dependency refers to.
51 This must be generated without loading the module."""
52 raise NotImplementedError()
42 53
43 54
44 def load_recipe_modules(mod_dirs): 55 class PathDependency(Dependency):
45 """Makes a python module object that have all recipe modules in its dict. 56 def __init__(self, path, local_name, base_path=None):
57 self._path = _normalize_path(base_path, path)
58 self._local_name = local_name
46 59
47 Args: 60 # We forbid modules from living outside our main paths to keep clients
48 mod_dirs (list of str): list of module search paths. 61 # from going crazy before we have standardized recipe locations.
49 """ 62 mod_dir = os.path.dirname(path)
50 def patchup_module(name, submod): 63 assert mod_dir in MODULE_DIRS(), (
51 """Finds framework related classes and functions in a |submod| and adds 64 'Modules living outside of approved directories are forbidden: '
52 them to |submod| as top level constants with well known names such as 65 '%s is not in %s' % (mod_dir, MODULE_DIRS()))
53 API, CONFIG_CTX and TEST_API.
54 66
55 |submod| is a recipe module (akin to python package) with submodules such as 67 def load(self, universe):
56 'api', 'config', 'test_api'. This function scans through dicts of that 68 return _load_recipe_module_module(self._path, universe)
57 submodules to find subclasses of RecipeApi, RecipeTestApi, etc. 69
70 @property
71 def local_name(self):
72 return self._local_name
73
74 @property
75 def unique_name(self):
76 return self._path
77
78
79 class NamedDependency(PathDependency):
80 def __init__(self, name):
81 for path in MODULE_DIRS():
82 mod_path = os.path.join(path, name)
83 if os.path.exists(os.path.join(mod_path, '__init__.py')):
84 super(NamedDependency, self).__init__(mod_path, name)
85 return
86 raise NoSuchRecipe('Recipe module named %s does not exist' % name)
87
88
89 class RecipeUniverse(object):
90 def __init__(self):
91 self._loaded = {}
92
93 def load(self, dep):
94 """Load a Dependency."""
95 name = dep.unique_name
96 if name in self._loaded:
97 mod = self._loaded[name]
98 assert mod is not None, (
99 'Cyclic dependency when trying to load %s' % name)
100 return mod
101 else:
102 self._loaded[name] = None
103 mod = dep.load(self)
104 self._loaded[name] = mod
105 return mod
106
107 def deps_from_names(self, deps):
108 """Load dependencies given a list simple module names (old style)."""
109 return { dep: self.load(NamedDependency(dep)) for dep in deps }
110
111 def deps_from_paths(self, deps, base_path):
112 """Load dependencies given a dictionary of local names to module paths
113 (new style)."""
114 return { name: self.load(PathDependency(path, name, base_path))
115 for name, path in deps.iteritems() }
116
117 def deps_from_mixed(self, deps, base_path):
118 """Load dependencies given either a new style or old style deps spec."""
119 if isinstance(deps, (list, tuple)):
120 return self.deps_from_names(deps)
121 elif isinstance(deps, dict):
122 return self.deps_from_paths(deps, base_path)
123 else:
124 raise ValueError('%s is not a valid or known deps structure' % deps)
125
126 def load_recipe(self, recipe):
127 """Given name of a recipe, loads and returns it as RecipeScript instance.
128
129 Args:
130 recipe (str): name of a recipe, can be in form '<module>:<recipe>'.
131
132 Returns:
133 RecipeScript instance.
134
135 Raises:
136 NoSuchRecipe: recipe is not found.
58 """ 137 """
59 submod.NAME = name 138 # If the recipe is specified as "module:recipe", then it is an recipe
60 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod)) 139 # contained in a recipe_module as an example. Look for it in the modules
61 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None) 140 # imported by load_recipe_modules instead of the normal search paths.
62 submod.DEPS = frozenset(getattr(submod, 'DEPS', ())) 141 if ':' in recipe:
142 module_name, example = recipe.split(':')
143 assert example.endswith('example')
144 for module_dir in MODULE_DIRS():
145 for subitem in os.listdir(module_dir):
146 if module_name == subitem:
147 return RecipeScript.from_script_path(
148 os.path.join(module_dir, subitem, 'example.py'), self)
149 raise NoSuchRecipe(recipe,
150 'Recipe example %s:%s does not exist' %
151 (module_name, example))
152 else:
153 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()):
154 if os.path.exists(recipe_path + '.py'):
155 return RecipeScript.from_script_path(recipe_path + '.py', self)
156 raise NoSuchRecipe(recipe)
63 157
64 if hasattr(submod, 'config'):
65 for v in submod.config.__dict__.itervalues():
66 if isinstance(v, ConfigContext):
67 assert not submod.CONFIG_CTX, (
68 'More than one configuration context: %s' % (submod.config))
69 submod.CONFIG_CTX = v
70 assert submod.CONFIG_CTX, 'Config file, but no config context?'
71 158
72 submod.API = getattr(submod, 'API', None)
73 for v in submod.api.__dict__.itervalues():
74 if inspect.isclass(v) and issubclass(v, RecipeApiPlain):
75 assert not submod.API, (
76 'More than one Api subclass: %s' % submod.api)
77 submod.API = v
78 assert submod.API, 'Submodule has no api? %s' % (submod)
79 159
80 submod.TEST_API = getattr(submod, 'TEST_API', None)
81 if hasattr(submod, 'test_api'):
82 for v in submod.test_api.__dict__.itervalues():
83 if inspect.isclass(v) and issubclass(v, RecipeTestApi):
84 assert not submod.TEST_API, (
85 'More than one TestApi subclass: %s' % submod.api)
86 submod.TEST_API = v
87 assert submod.API, (
88 'Submodule has test_api.py but no TestApi subclass? %s'
89 % (submod)
90 )
91 160
92 RM = 'RECIPE_MODULES' 161 def _find_and_load_module(fullname, modname, path):
93 def find_and_load(fullname, modname, path): 162 imp.acquire_lock()
94 if fullname not in sys.modules or fullname == RM: 163 try:
164 if fullname not in sys.modules:
95 fil = None 165 fil = None
96 try: 166 try:
97 fil, pathname, descr = imp.find_module(modname, 167 fil, pathname, descr = imp.find_module(modname,
98 [os.path.dirname(path)]) 168 [os.path.dirname(path)])
99 imp.load_module(fullname, fil, pathname, descr) 169 imp.load_module(fullname, fil, pathname, descr)
100 finally: 170 finally:
101 if fil: 171 if fil:
102 fil.close() 172 fil.close()
103 return sys.modules[fullname] 173 return sys.modules[fullname]
104
105 def recursive_import(path, prefix=None, skip_fn=lambda name: False):
106 modname = os.path.splitext(os.path.basename(path))[0]
107 if prefix:
108 fullname = '%s.%s' % (prefix, modname)
109 else:
110 fullname = RM
111 m = find_and_load(fullname, modname, path)
112 if not os.path.isdir(path):
113 return m
114
115 for subitem in os.listdir(path):
116 subpath = os.path.join(path, subitem)
117 subname = os.path.splitext(subitem)[0]
118 if skip_fn(subname):
119 continue
120 if os.path.isdir(subpath):
121 if not os.path.exists(os.path.join(subpath, '__init__.py')):
122 continue
123 elif not subpath.endswith('.py') or subitem.startswith('__init__.py'):
124 continue
125
126 submod = recursive_import(subpath, fullname, skip_fn=skip_fn)
127
128 if not hasattr(m, subname):
129 setattr(m, subname, submod)
130 else:
131 prev = getattr(m, subname)
132 assert submod is prev, (
133 'Conflicting modules: %s and %s' % (prev, m))
134
135 return m
136
137 imp.acquire_lock()
138 try:
139 if RM not in sys.modules:
140 sys.modules[RM] = imp.new_module(RM)
141 # First import all the APIs and configs
142 for root in mod_dirs:
143 if os.path.isdir(root):
144 recursive_import(root, skip_fn=lambda name: name.endswith('_config'))
145
146 # Then fixup all the modules
147 for name, submod in sys.modules[RM].__dict__.iteritems():
148 if name[0] == '_':
149 continue
150 patchup_module(name, submod)
151
152 # Then import all the config extenders.
153 for root in mod_dirs:
154 if os.path.isdir(root):
155 recursive_import(root)
156 return sys.modules[RM]
157 finally: 174 finally:
158 imp.release_lock() 175 imp.release_lock()
159 176
160 177
161 def create_apis(mod_dirs, names, only_test_api, engine, test_data): 178 def _load_recipe_module_module(path, universe):
162 """Given a list of module names, return linked instances of RecipeApi 179 modname = os.path.splitext(os.path.basename(path))[0]
163 and RecipeTestApi (in a pair) which contains those modules as direct members. 180 fullname = '%s.%s' % (RECIPE_MODULE_PREFIX, modname)
181 mod = _find_and_load_module(fullname, modname, path)
164 182
165 So, if you pass ['foobar'], you'll get an instance back which contains a 183 # This actually loads the dependencies.
166 'foobar' attribute which itself is a RecipeApi instance from the 'foobar' 184 mod.LOADED_DEPS = universe.deps_from_mixed(
167 module. 185 getattr(mod, 'DEPS', []), os.path.basename(path))
168 186
169 Args: 187 # TODO(luqui): Remove this hack once configs are cleaned.
170 mod_dirs (list): A list of paths to directories which contain modules. 188 sys.modules['%s.DEPS' % fullname] = mod.LOADED_DEPS
171 names (list): A list of module names to include in the returned RecipeApi. 189 _recursive_import(path, RECIPE_MODULE_PREFIX)
172 only_test_api (bool): If True, do not create RecipeApi, only RecipeTestApi. 190 _patchup_module(modname, mod)
173 engine (object): A recipe engine instance that gets passed to each API.
174 Among other things it provides:
175 properties (dict): the properties dictionary (used by the properties
176 module)
177 See annotated_run.py for definition.
178 test_data (TestData): ...
179 191
180 Returns: 192 return mod
181 Pair (RecipeApi instance or None, RecipeTestApi instance).
182 """
183 recipe_modules = load_recipe_modules(mod_dirs)
184
185 # Recipe module name (or None for top level API) -> RecipeTestApi instance.
186 test_apis = {}
187 # Recipe module name (or None for top level API) -> RecipeApi instance.
188 apis = {}
189
190 # 'None' keys represent top level API objects returned by this function.
191 test_apis[None] = RecipeTestApi(module=None)
192 if not only_test_api:
193 apis[None] = RecipeApi(module=None,
194 engine=engine,
195 test_data=test_data.get_module_test_data(None))
196
197 dep_map = {None: set(names)}
198 def create_maps(name):
199 if name not in dep_map:
200 module = getattr(recipe_modules, name)
201
202 dep_map[name] = set(module.DEPS)
203 map(create_maps, dep_map[name])
204
205 test_api_cls = getattr(module, 'TEST_API', None) or RecipeTestApi
206 test_apis[name] = test_api_cls(module=module)
207
208 if not only_test_api:
209 api_cls = getattr(module, 'API')
210 apis[name] = api_cls(module=module,
211 engine=engine,
212 test_data=test_data.get_module_test_data(name))
213
214 map(create_maps, names)
215
216 map_dependencies(dep_map, test_apis)
217 if not only_test_api:
218 map_dependencies(dep_map, apis)
219 for name, module in apis.iteritems():
220 module.test_api = test_apis[name]
221
222 return apis.get(None), test_apis.get(None)
223 193
224 194
225 def map_dependencies(dep_map, inst_map): 195 def _recursive_import(path, prefix):
226 # NOTE: this is 'inefficient', but correct and compact. 196 modname = os.path.splitext(os.path.basename(path))[0]
227 dep_map = copy.deepcopy(dep_map) 197 fullname = '%s.%s' % (prefix, modname)
228 while dep_map: 198 mod = _find_and_load_module(fullname, modname, path)
229 did_something = False 199 if not os.path.isdir(path):
230 to_pop = [] 200 return mod
231 for api_name, deps in dep_map.iteritems(): 201
232 to_remove = [] 202 for subitem in os.listdir(path):
233 for dep in [d for d in deps if d not in dep_map]: 203 subpath = os.path.join(path, subitem)
234 # Grab the injection site 204 subname = os.path.splitext(subitem)[0]
235 obj = inst_map[api_name].m 205 if os.path.isdir(subpath):
236 assert not hasattr(obj, dep) 206 if not os.path.exists(os.path.join(subpath, '__init__.py')):
237 setattr(obj, dep, inst_map[dep]) 207 continue
238 to_remove.append(dep) 208 elif not subpath.endswith('.py') or subitem.startswith('__init__.py'):
239 did_something = True 209 continue
240 map(deps.remove, to_remove) 210
241 if not deps: 211 submod = _recursive_import(subpath, fullname)
242 to_pop.append(api_name) 212
243 did_something = True 213 if not hasattr(mod, subname):
244 map(dep_map.pop, to_pop) 214 setattr(mod, subname, submod)
245 assert did_something, 'Did nothing on this loop. %s' % dep_map 215 else:
216 prev = getattr(mod, subname)
217 assert submod is prev, (
218 'Conflicting modules: %s and %s' % (prev, mod))
219
220 return mod
246 221
247 222
248 def create_recipe_api(names, engine, test_data=DisabledTestData()): 223 def _patchup_module(name, submod):
249 return create_apis(MODULE_DIRS(), names, False, engine, test_data)[0] 224 """Finds framework related classes and functions in a |submod| and adds
225 them to |submod| as top level constants with well known names such as
226 API, CONFIG_CTX and TEST_API.
227
228 |submod| is a recipe module (akin to python package) with submodules such as
229 'api', 'config', 'test_api'. This function scans through dicts of that
230 submodules to find subclasses of RecipeApi, RecipeTestApi, etc.
231 """
232 submod.NAME = name
233 submod.UNIQUE_NAME = name # TODO(luqui): use a luci-config unique name
234 submod.MODULE_DIRECTORY = Path(ModuleBasePath(submod))
235 submod.CONFIG_CTX = getattr(submod, 'CONFIG_CTX', None)
236
237 if hasattr(submod, 'config'):
238 for v in submod.config.__dict__.itervalues():
239 if isinstance(v, ConfigContext):
240 assert not submod.CONFIG_CTX, (
241 'More than one configuration context: %s, %s' %
242 (submod.config, submod.CONFIG_CTX))
243 submod.CONFIG_CTX = v
244 assert submod.CONFIG_CTX, 'Config file, but no config context?'
245
246 submod.API = getattr(submod, 'API', None)
247 for v in submod.api.__dict__.itervalues():
248 if inspect.isclass(v) and issubclass(v, RecipeApiPlain):
249 assert not submod.API, (
250 '%s has more than one Api subclass: %s, %s' % (name, v, submod.api))
251 submod.API = v
252 assert submod.API, 'Submodule has no api? %s' % (submod)
253
254 submod.TEST_API = getattr(submod, 'TEST_API', None)
255 if hasattr(submod, 'test_api'):
256 for v in submod.test_api.__dict__.itervalues():
257 if inspect.isclass(v) and issubclass(v, RecipeTestApi):
258 assert not submod.TEST_API, (
259 'More than one TestApi subclass: %s' % submod.api)
260 submod.TEST_API = v
261 assert submod.API, (
262 'Submodule has test_api.py but no TestApi subclass? %s'
263 % (submod)
264 )
250 265
251 266
252 def create_test_api(names): 267 class DependencyMapper(object):
253 # Test API should not use runtime engine or test_data, do not pass it. 268 """DependencyMapper topologically traverses the dependency DAG beginning at
254 return create_apis(MODULE_DIRS(), names, True, None, DisabledTestData())[1] 269 a module, executing a callback ("instantiator") for each module.
270
271 For example, if the dependency DAG looked like this:
272
273 A
274 / \
275 B C
276 \ /
277 D
278
279 (with D depending on B and C, etc.), DependencyMapper(f).instantiate(D) would
280 construct
281
282 f_A = f(A, {})
283 f_B = f(B, { 'A': f_A })
284 f_C = f(C, { 'A': f_A })
285 f_D = f(D, { 'B': f_B, 'C': f_C })
286
287 finally returning f_D. instantiate can be called multiple times, which reuses
288 already-computed results.
289 """
290
291 def __init__(self, instantiator):
292 self._instantiator = instantiator
293 self._instances = {}
294
295 def instantiate(self, mod):
296 if mod in self._instances:
297 return self._instances[mod]
298 deps_dict = { name: self.instantiate(dep)
299 for name, dep in mod.LOADED_DEPS.iteritems() }
300 self._instances[mod] = self._instantiator(mod, deps_dict)
301 return self._instances[mod]
255 302
256 303
257 @cached_unary 304 def create_recipe_api(toplevel_deps, engine, test_data=DisabledTestData()):
258 def load_recipe(recipe): 305 def instantiator(mod, deps):
259 """Given name of a recipe, loads and returns it as RecipeScript instance. 306 # TODO(luqui): test_data will need to use canonical unique names.
307 mod_api = mod.API(module=mod, engine=engine,
308 test_data=test_data.get_module_test_data(mod.NAME))
309 mod_api.test_api = (getattr(mod, 'TEST_API', None)
310 or RecipeTestApi)(module=mod)
311 for k, v in deps.iteritems():
312 setattr(mod_api.m, k, v)
313 setattr(mod_api.test_api.m, k, v.test_api)
314 return mod_api
260 315
261 Args: 316 mapper = DependencyMapper(instantiator)
262 recipe (str): name of a recipe, can be in form '<module>:<recipe>'. 317 api = RecipeApi(module=None, engine=engine,
318 test_data=test_data.get_module_test_data(None))
319 for k, v in toplevel_deps.iteritems():
320 setattr(api, k, mapper.instantiate(v))
321 return api
263 322
264 Returns:
265 RecipeScript instance.
266 323
267 Raises: 324 def create_test_api(toplevel_deps, universe):
268 NoSuchRecipe: recipe is not found. 325 def instantiator(mod, deps):
269 """ 326 modapi = (getattr(mod, 'TEST_API', None) or RecipeTestApi)(module=mod)
270 # If the recipe is specified as "module:recipe", then it is an recipe 327 for k,v in deps.iteritems():
271 # contained in a recipe_module as an example. Look for it in the modules 328 setattr(modapi.m, k, v)
272 # imported by load_recipe_modules instead of the normal search paths. 329 return modapi
273 if ':' in recipe: 330
274 module_name, example = recipe.split(':') 331 mapper = DependencyMapper(instantiator)
275 assert example.endswith('example') 332 api = RecipeTestApi(module=None)
276 RECIPE_MODULES = load_recipe_modules(MODULE_DIRS()) 333 for k,v in toplevel_deps.iteritems():
277 try: 334 setattr(api, k, mapper.instantiate(v))
278 script_module = getattr(getattr(RECIPE_MODULES, module_name), example) 335 return api
279 return RecipeScript.from_module_object(script_module) 336
280 except AttributeError: 337
281 raise NoSuchRecipe(recipe, 338 def loop_over_recipe_modules():
282 'Recipe module %s does not have example %s defined' % 339 for path in MODULE_DIRS():
283 (module_name, example)) 340 if os.path.isdir(path):
284 else: 341 for item in os.listdir(path):
285 for recipe_path in (os.path.join(p, recipe) for p in RECIPE_DIRS()): 342 subpath = os.path.join(path, item)
286 if os.path.exists(recipe_path + '.py'): 343 if os.path.isdir(subpath):
287 return RecipeScript.from_script_path(recipe_path + '.py') 344 yield subpath
288 raise NoSuchRecipe(recipe)
289 345
290 346
291 def loop_over_recipes(): 347 def loop_over_recipes():
292 """Yields pairs (path to recipe, recipe name). 348 """Yields pairs (path to recipe, recipe name).
293 349
294 Enumerates real recipes in recipes/* as well as examples in recipe_modules/*. 350 Enumerates real recipes in recipes/* as well as examples in recipe_modules/*.
295 """ 351 """
296 for path in RECIPE_DIRS(): 352 for path in RECIPE_DIRS():
297 for recipe in scan_directory( 353 for recipe in scan_directory(
298 path, lambda f: f.endswith('.py') and f[0] != '_'): 354 path, lambda f: f.endswith('.py') and f[0] != '_'):
299 yield recipe, recipe[len(path)+1:-len('.py')] 355 yield recipe, recipe[len(path)+1:-len('.py')]
300 for path in MODULE_DIRS(): 356 for path in MODULE_DIRS():
301 for recipe in scan_directory( 357 for recipe in scan_directory(
302 path, lambda f: f.endswith('example.py')): 358 path, lambda f: f.endswith('example.py')):
303 module_name = os.path.dirname(recipe)[len(path)+1:] 359 module_name = os.path.dirname(recipe)[len(path)+1:]
304 yield recipe, '%s:example' % module_name 360 yield recipe, '%s:example' % module_name
361
362
363 def _normalize_path(base_path, path):
364 if base_path is None or os.path.isabs(path):
365 return os.path.realpath(path)
366 else:
367 return os.path.realpath(os.path.join(base_path, path))
368
OLDNEW
« no previous file with comments | « scripts/slave/recipe_config_types.py ('k') | scripts/slave/recipe_modules/__init__.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698