281 lines
8.5 KiB
Python
281 lines
8.5 KiB
Python
# Copyright 2012 Google Inc. All Rights Reserved.
|
|
|
|
"""Project representation.
|
|
|
|
A project is a module (or set of modules) that provides a namespace of rules.
|
|
Rules may refer to each other and will be resolved in the project namespace.
|
|
"""
|
|
|
|
__author__ = 'benvanik@google.com (Ben Vanik)'
|
|
|
|
|
|
import base64
|
|
import os
|
|
import pickle
|
|
import re
|
|
import stat
|
|
import string
|
|
|
|
from anvil.module import ModuleLoader
|
|
from anvil.rule import RuleNamespace
|
|
import anvil.util
|
|
|
|
|
|
class Project(object):
|
|
"""Project type that contains rules.
|
|
Projects, once constructed, are designed to be immutable. Many duplicate
|
|
build processes may run over the same project instance and all expect it to
|
|
be in the state it was when first created.
|
|
"""
|
|
|
|
def __init__(self, name='Project', rule_namespace=None, module_resolver=None,
|
|
modules=None):
|
|
"""Initializes an empty project.
|
|
|
|
Args:
|
|
name: A human-readable name for the project that will be used for
|
|
logging.
|
|
rule_namespace: Rule namespace to use when loading modules. If omitted a
|
|
default one is used.
|
|
module_resolver: A module resolver to use when attempt to dynamically
|
|
resolve modules by path.
|
|
modules: A list of modules to add to the project.
|
|
|
|
Raises:
|
|
NameError: The name given is not valid.
|
|
"""
|
|
self.name = name
|
|
|
|
if rule_namespace:
|
|
self.rule_namespace = rule_namespace
|
|
else:
|
|
self.rule_namespace = RuleNamespace()
|
|
self.rule_namespace.discover()
|
|
|
|
if module_resolver:
|
|
self.module_resolver = module_resolver
|
|
else:
|
|
self.module_resolver = StaticModuleResolver()
|
|
|
|
self.modules = {}
|
|
if modules and len(modules):
|
|
self.add_modules(modules)
|
|
|
|
def add_module(self, module):
|
|
"""Adds a module to the project.
|
|
|
|
Args:
|
|
module: A module to add.
|
|
|
|
Raises:
|
|
KeyError: A module with the given name already exists in the project.
|
|
"""
|
|
self.add_modules([module])
|
|
|
|
def add_modules(self, modules):
|
|
"""Adds a list of modules to the project.
|
|
|
|
Args:
|
|
modules: A list of modules to add.
|
|
|
|
Raises:
|
|
KeyError: A module with the given name already exists in the project.
|
|
"""
|
|
for module in modules:
|
|
if self.modules.get(module.path, None):
|
|
raise KeyError('A module with the path "%s" is already defined' % (
|
|
module.path))
|
|
for module in modules:
|
|
self.modules[module.path] = module
|
|
|
|
def get_module(self, module_path):
|
|
"""Gets a module by path.
|
|
|
|
Args:
|
|
module_path: Name of the module to find.
|
|
|
|
Returns:
|
|
The module with the given path or None if it was not found.
|
|
"""
|
|
return self.modules.get(module_path, None)
|
|
|
|
def module_list(self):
|
|
"""Gets a list of all modules in the project.
|
|
|
|
Returns:
|
|
A list of all modules.
|
|
"""
|
|
return self.modules.values()
|
|
|
|
def module_iter(self):
|
|
"""Iterates over all modules in the project."""
|
|
for module_path in self.modules:
|
|
yield self.modules[module_path]
|
|
|
|
def resolve_rule(self, rule_path, requesting_module=None):
|
|
"""Gets a rule by path, supporting module lookup and dynamic loading.
|
|
|
|
Args:
|
|
rule_path: Path of the rule to find. Must include a semicolon.
|
|
requesting_module: The module that is requesting the given rule. If not
|
|
provided then no local rule paths (':foo') or relative paths are
|
|
allowed.
|
|
|
|
Returns:
|
|
The rule with the given name or None if it was not found.
|
|
|
|
Raises:
|
|
NameError: The given rule name was not valid.
|
|
KeyError: The given rule was not found.
|
|
IOError: Unable to load referenced module.
|
|
"""
|
|
if not anvil.util.is_rule_path(rule_path):
|
|
raise NameError('The rule path "%s" is missing a semicolon' % (rule_path))
|
|
(module_path, rule_name) = rule_path.split(':')
|
|
print(rule_name)
|
|
if self.module_resolver.can_resolve_local:
|
|
if not len(module_path) and not requesting_module:
|
|
module_path = '.'
|
|
if not len(module_path) and not requesting_module:
|
|
raise KeyError('Local rule "%s" given when no resolver defined' % (
|
|
rule_path))
|
|
|
|
module = requesting_module
|
|
if len(module_path):
|
|
requesting_path = None
|
|
if requesting_module:
|
|
requesting_path = os.path.dirname(requesting_module.path)
|
|
full_path = self.module_resolver.resolve_module_path(
|
|
module_path, requesting_path)
|
|
module = self.modules.get(full_path, None)
|
|
if not module:
|
|
# Module not yet loaded - need to grab it
|
|
module = self.module_resolver.load_module(
|
|
full_path, self.rule_namespace)
|
|
if module:
|
|
self.add_module(module)
|
|
else:
|
|
raise IOError('Module "%s" not found', module_path)
|
|
|
|
return module.get_rule(rule_name)
|
|
|
|
|
|
class ModuleResolver(object):
|
|
"""A type to use for resolving modules.
|
|
This is used to get a module when a project tries to resolve a rule in a
|
|
module that has not yet been loaded.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initializes a module resolver."""
|
|
self.can_resolve_local = False
|
|
|
|
def resolve_module_path(self, path, working_path=None):
|
|
"""Resolves a module path to its full, absolute path.
|
|
This is used by the project system to disambugate modules and check the
|
|
cache before actually performing a load.
|
|
The path returned from this will be passed to load_module.
|
|
|
|
Args:
|
|
path: Path of the module (may be relative/etc).
|
|
working_path: Path relative paths should be pased off of. If not provided
|
|
then relative paths may fail.
|
|
|
|
Returns:
|
|
An absolute path that can be used as a cache key and passed to
|
|
load_module.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def load_module(self, full_path, rule_namespace):
|
|
"""Loads a module from the given path.
|
|
|
|
Args:
|
|
full_path: Absolute path of the module as returned by resolve_module_path.
|
|
rule_namespace: Rule namespace to use when loading modules.
|
|
|
|
Returns:
|
|
A Module representing the given path or None if it could not be found.
|
|
|
|
Raises:
|
|
IOError: The module could not be found.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class StaticModuleResolver(ModuleResolver):
|
|
"""A static module resolver that can resolve from a list of modules.
|
|
"""
|
|
|
|
def __init__(self, modules=None, *args, **kwargs):
|
|
"""Initializes a static module resolver.
|
|
|
|
Args:
|
|
modules: A list of modules that can be resolved.
|
|
"""
|
|
super(StaticModuleResolver, self).__init__(*args, **kwargs)
|
|
|
|
self.modules = {}
|
|
if modules:
|
|
for module in modules:
|
|
self.modules[os.path.normpath(module.path)] = module
|
|
|
|
def resolve_module_path(self, path, working_path=None):
|
|
real_path = path
|
|
if working_path and len(working_path):
|
|
real_path = os.path.join(working_path, path)
|
|
return os.path.normpath(real_path)
|
|
|
|
def load_module(self, full_path, rule_namespace):
|
|
return self.modules.get(os.path.normpath(full_path), None)
|
|
|
|
|
|
class FileModuleResolver(ModuleResolver):
|
|
"""A file-system backed module resolver.
|
|
|
|
Rules are searched for with relative paths from a defined root path.
|
|
If the module path given is a directory, the resolver will attempt to load
|
|
a BUILD file from that directory - otherwise the file specified will be
|
|
treated as the module.
|
|
"""
|
|
|
|
def __init__(self, root_path, *args, **kwargs):
|
|
"""Initializes a file-system module resolver.
|
|
|
|
Args:
|
|
root_path: Root filesystem path to treat as the base for all resolutions.
|
|
|
|
Raises:
|
|
IOError: The given root path is not found or is not a directory.
|
|
"""
|
|
super(FileModuleResolver, self).__init__(*args, **kwargs)
|
|
|
|
self.can_resolve_local = True
|
|
|
|
self.root_path = os.path.normpath(root_path)
|
|
if not os.path.isdir(self.root_path):
|
|
raise IOError('Root path "%s" not found' % (self.root_path))
|
|
|
|
def resolve_module_path(self, path, working_path=None):
|
|
# Compute the real path
|
|
has_working_path = working_path and len(working_path)
|
|
real_path = path
|
|
if has_working_path:
|
|
real_path = os.path.join(working_path, path)
|
|
real_path = os.path.normpath(real_path)
|
|
full_path = os.path.join(self.root_path, real_path)
|
|
full_path = os.path.normpath(full_path)
|
|
|
|
# Check to see if it exists and is a file
|
|
# Special handling to find BUILD files under directories
|
|
full_path = anvil.util.get_build_file_path(full_path)
|
|
if not os.path.isfile(full_path):
|
|
raise IOError('Path "%s" is not a file' % (full_path))
|
|
|
|
return os.path.normpath(full_path)
|
|
|
|
def load_module(self, full_path, rule_namespace):
|
|
module_loader = ModuleLoader(full_path, rule_namespace=rule_namespace)
|
|
module_loader.load()
|
|
return module_loader.execute()
|