# # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # # # gen_base.py -- infrastructure for generating makefiles, dependencies, etc. # import collections import os import sys import glob import re import fileinput import filecmp try: # Python >=3.0 import configparser except ImportError: # Python <3.0 import ConfigParser as configparser import generator.swig import getversion def _warning(msg): sys.stderr.write("WARNING: %s\n" % msg) def _error(msg): sys.stderr.write("ERROR: %s\n" % msg) sys.exit(1) class GeneratorBase: # # Derived classes should define a class attribute named _extension_map. # This attribute should be a dictionary of the form: # { (target-type, file-type): file-extension ...} # # where: target-type is 'exe', 'lib', ... # file-type is 'target', 'object', ... # def __init__(self, fname, verfname, options=None): # Retrieve major version from the C header, to avoid duplicating it in # build.conf - it is required because some file names include it. try: vsn_parser = getversion.Parser() vsn_parser.search('SVN_VER_MAJOR', 'libver') self.version = vsn_parser.parse(verfname).libver except: raise GenError('Unable to extract version.') # Read options self.release_mode = None for opt, val in options: if opt == '--release': self.release_mode = 1 # Now read and parse build.conf parser = configparser.ConfigParser() parser.readfp(open(fname)) self.conf = build_path(os.path.abspath(fname)) self.sections = { } self.graph = DependencyGraph() # Allow derived classes to suppress certain configuration sections if not hasattr(self, 'skip_sections'): self.skip_sections = { } # The 'options' section does not represent a build target, # it simply contains global options self.skip_sections['options'] = None # Read in the global options self.includes = \ _collect_paths(parser.get('options', 'includes')) self.private_includes = \ _collect_paths(parser.get('options', 'private-includes')) self.private_built_includes = \ parser.get('options', 'private-built-includes').split() self.scripts = \ _collect_paths(parser.get('options', 'test-scripts')) self.bdb_scripts = \ _collect_paths(parser.get('options', 'bdb-test-scripts')) self.include_wildcards = \ parser.get('options', 'include-wildcards').split() self.swig_lang = parser.get('options', 'swig-languages').split() self.swig_dirs = parser.get('options', 'swig-dirs').split() # SWIG Generator self.swig = generator.swig.Generator(self.conf, "swig") # Visual C++ projects - contents are either TargetProject instances, # or other targets with an external-project attribute. self.projects = [] # Lists of pathnames of various kinds self.test_deps = [] # Non-BDB dependent items to build for the tests self.test_progs = [] # Subset of the above to actually execute self.test_helpers = [] # $ {test_deps} \setminus {test_progs} $ self.bdb_test_deps = [] # BDB-dependent items to build for the tests self.bdb_test_progs = [] # Subset of the above to actually execute self.target_dirs = [] # Directories in which files are built self.manpages = [] # Manpages # Collect the build targets and have a reproducible ordering parser_sections = sorted(parser.sections()) for section_name in parser_sections: if section_name in self.skip_sections: continue options = {} for option in parser.options(section_name): options[option] = parser.get(section_name, option) type = options.get('type') target_class = _build_types.get(type) if not target_class: raise GenError('ERROR: unknown build type for ' + section_name) section = target_class.Section(target_class, section_name, options, self) self.sections[section_name] = section section.create_targets() # Compute intra-library dependencies for section in self.sections.values(): dependencies = (( DT_LINK, section.options.get('libs', "") ), ( DT_NONLIB, section.options.get('nonlibs', "") )) for dep_type, dep_names in dependencies: # Translate string names to Section objects dep_section_objects = [] for section_name in dep_names.split(): if section_name in self.sections: dep_section_objects.append(self.sections[section_name]) # For each dep_section that this section declares a dependency on, # take the targets of this section, and register a dependency on # any 'matching' targets of the dep_section. # # At the moment, the concept of multiple targets per section is # employed only for the SWIG modules, which have 1 target # per language. Then, 'matching' means being of the same language. for dep_section in dep_section_objects: for target in section.get_targets(): self.graph.bulk_add(dep_type, target.name, dep_section.get_dep_targets(target)) def compute_hdrs(self): """Get a list of the header files""" all_includes = list(map(native_path, self.includes + self.private_includes)) for d in unique(self.target_dirs): for wildcard in self.include_wildcards: hdrs = glob.glob(os.path.join(native_path(d), wildcard)) all_includes.extend(hdrs) return all_includes def compute_hdr_deps(self): """Compute the dependencies of each header file""" include_deps = IncludeDependencyInfo(self.compute_hdrs(), list(map(native_path, self.private_built_includes))) for objectfile, sources in self.graph.get_deps(DT_OBJECT): assert len(sources) == 1 source = sources[0] # Generated .c files must depend on all headers their parent .i file # includes if isinstance(objectfile, SWIGObject): swigsources = self.graph.get_sources(DT_SWIG_C, source) assert len(swigsources) == 1 ifile = swigsources[0] assert isinstance(ifile, SWIGSource) c_includes, swig_includes = \ include_deps.query_swig(native_path(ifile.filename)) for include_file in c_includes: self.graph.add(DT_OBJECT, objectfile, build_path(include_file)) for include_file in swig_includes: self.graph.add(DT_SWIG_C, source, build_path(include_file)) # Any non-swig C/C++ object must depend on the headers its parent # .c or .cpp includes. Note that 'object' includes gettext .mo files, # Java .class files, and .h files generated from Java classes, so # we must filter here. elif isinstance(source, SourceFile) and \ os.path.splitext(source.filename)[1] in ('.c', '.cpp'): for include_file in include_deps.query(native_path(source.filename)): self.graph.add(DT_OBJECT, objectfile, build_path(include_file)) def write_sqlite_headers(self): "Transform sql files into header files" import transform_sql for hdrfile, sqlfile in self.graph.get_deps(DT_SQLHDR): new_hdrfile = hdrfile + ".new" new_file = open(new_hdrfile, 'w') transform_sql.main(sqlfile[0], new_file) new_file.close() def identical(file1, file2): try: if filecmp.cmp(new_hdrfile, hdrfile): return True else: return False except: return False if identical(new_hdrfile, hdrfile): os.remove(new_hdrfile) else: try: os.remove(hdrfile) except: pass os.rename(new_hdrfile, hdrfile) def write_file_if_changed(self, fname, new_contents): """Rewrite the file if NEW_CONTENTS are different than its current content. If you have your windows projects open and generate the projects it's not a small thing for windows to re-read all projects so only update those that have changed. Under Python >=3, NEW_CONTENTS must be a 'str', not a 'bytes'. """ if sys.version_info[0] >= 3: new_contents = new_contents.encode() try: old_contents = open(fname, 'rb').read() except IOError: old_contents = None if old_contents != new_contents: open(fname, 'wb').write(new_contents) print("Wrote: %s" % fname) def write_errno_table(self): # ### We generate errorcode.inc at autogen.sh time (here!). # ### # ### Currently it's only used by maintainer-mode builds. If this # ### functionality ever moves to release builds, it will have to move # ### to configure-time (but remember that Python cannot be assumed to # ### be available from 'configure'). import errno lines = [ '/* This file was generated by build/generator/gen_base.py */', '' ] def write_struct(name, codes): lines.extend([ 'static struct {', ' int errcode;', ' const char *errname;', '} %s[] = {' % (name,), ]) for num, val in sorted(codes): lines.extend([ ' { %d, "%s" },' % (num, val), ]) # Remove ',' for c89 compatibility lines[-1] = lines[-1][0:-1] lines.extend([ '};', '', ]) # errno names can vary depending on the Python, and possibly the # OS, version and they are not even used by normal release builds # so omit them from the tarball. We always want the struct itself # so that SVN_DEBUG builds still compile and it needs a dummy # entry to avoid a zero-sized array. write_struct('svn__errno', [(0, "success")] if self.release_mode else errno.errorcode.items()) # Fetch and write apr_errno.h codes. aprerr = [] for line in open(os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'tools', 'dev', 'aprerr.txt')): # aprerr.txt parsing duplicated in which-error.py if line.startswith('#'): continue key, _, val = line.split() aprerr += [(int(val), key)] write_struct('svn__apr_errno', aprerr) aprdict = dict(aprerr) del aprerr ## sanity check intersection = set(errno.errorcode.keys()) & set(aprdict.keys()) intersection = filter(lambda x: errno.errorcode[x] != aprdict[x], intersection) if self.errno_filter(intersection): print("WARNING: errno intersects APR error codes; " "runtime computation of symbolic error names for the following numeric codes might be wrong: " "%r" % (intersection,)) self.write_file_if_changed('subversion/libsvn_subr/errorcode.inc', '\n'.join(lines)) def errno_filter(self, codes): # list() to force the generator under python3 return list(codes) class FileSectionOptionEnum(object): # These are accessed via getattr() later on file = object() section = object() option = object() def _client_configuration_defines(self): """Return an iterator over SVN_CONFIG_* #define's in the "Client configuration files strings" section of svn_config.h.""" pattern = re.compile( r'^\s*#\s*define\s+' r'(?PSVN_CONFIG_(?PCATEGORY|SECTION|OPTION)_[A-Z0-9a-z_]+)' ) kind = { 'CATEGORY': self.FileSectionOptionEnum.file, 'SECTION': self.FileSectionOptionEnum.section, 'OPTION': self.FileSectionOptionEnum.option, } fname = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'subversion', 'include', 'svn_config.h') lines = iter(open(fname)) for line in lines: if "@name Client configuration files strings" in line: break else: raise Exception("Unable to parse svn_config.h") for line in lines: if "@{" in line: break else: raise Exception("Unable to parse svn_config.h") for line in lines: if "@}" in line: break match = pattern.match(line) if match: yield ( match.group('macro'), kind[match.group('kind')], ) else: raise Exception("Unable to parse svn_config.h") def write_config_keys(self): groupby = collections.defaultdict(list) empty_sections = [] previous = (None, None) for macro, kind in self._client_configuration_defines(): if kind is previous[1] is self.FileSectionOptionEnum.section: empty_sections.append(previous[0]) groupby[kind].append(macro) previous = (macro, kind) else: # If the last (macro, kind) is a section, then it's an empty section. if kind is self.FileSectionOptionEnum.section: empty_sections.append(macro) lines = [] lines.append('/* Automatically generated by %s:write_config_keys() */' % (__file__,)) lines.append('') for kind in ('file', 'section', 'option'): macros = groupby[getattr(self.FileSectionOptionEnum, kind)] lines.append('static const char *svn__valid_config_%ss[] = {' % (kind,)) for macro in macros: lines.append(' %s,' % (macro,)) # Remove ',' for c89 compatibility lines[-1] = lines[-1][0:-1] lines.append('};') lines.append('') lines.append('static const char *svn__empty_config_sections[] = {'); for section in empty_sections: lines.append(' %s,' % (section,)) # Remove ',' for c89 compatibility lines[-1] = lines[-1][0:-1] lines.append('};') lines.append('') self.write_file_if_changed('subversion/libsvn_subr/config_keys.inc', '\n'.join(lines)) class DependencyGraph: """Record dependencies between build items. See the DT_* values for the different dependency types. For each type, the target and source objects recorded will be different. They could be file names, Target objects, install types, etc. """ def __init__(self): self.deps = { } # type -> { target -> [ source ... ] } for dt in dep_types: self.deps[dt] = { } def add(self, type, target, source): if target in self.deps[type]: self.deps[type][target].append(source) else: self.deps[type][target] = [ source ] def remove(self, type, target, source): if target in self.deps[type] and source in self.deps[type][target]: self.deps[type][target].remove(source) def bulk_add(self, type, target, sources): if target in self.deps[type]: self.deps[type][target].extend(sources) else: self.deps[type][target] = sources[:] def get_sources(self, type, target, cls=None): sources = self.deps[type].get(target, [ ]) if not cls: return sources filtered = [ ] for src in sources: if isinstance(src, cls): filtered.append(src) return filtered def get_all_sources(self, type): sources = [ ] for group in self.deps[type].values(): sources.extend(group) return sources def get_deps(self, type): return list(self.deps[type].items()) # dependency types dep_types = [ 'DT_INSTALL', # install areas. e.g. 'lib', 'base-lib' 'DT_OBJECT', # an object filename, depending upon .c filenames 'DT_SWIG_C', # a swig-generated .c file, depending upon .i filename(s) 'DT_LINK', # a libtool-linked filename, depending upon object fnames 'DT_NONLIB', # filename depends on object fnames, but isn't linked to them 'DT_SQLHDR', # header generated from a .sql file ] # create some variables for these for _dt in dep_types: # e.g. DT_INSTALL = 'DT_INSTALL' globals()[_dt] = _dt class DependencyNode: def __init__(self, filename, when = None): self.filename = filename self.when = when def __str__(self): return self.filename class ObjectFile(DependencyNode): def __init__(self, filename, compile_cmd = None, when = None): DependencyNode.__init__(self, filename, when) self.compile_cmd = compile_cmd self.source_generated = 0 class SWIGObject(ObjectFile): def __init__(self, filename, lang, release_mode): ObjectFile.__init__(self, filename) self.lang = lang self.lang_abbrev = lang_abbrev[lang] # in release mode the sources are not generated by the build # but rather by the packager if release_mode: self.source_generated = 0 else: self.source_generated = 1 ### hmm. this is Makefile-specific self.compile_cmd = '$(COMPILE_%s_WRAPPER)' % self.lang_abbrev.upper() class HeaderFile(DependencyNode): def __init__(self, filename, classname = None, compile_cmd = None): DependencyNode.__init__(self, filename) self.classname = classname self.compile_cmd = compile_cmd class SourceFile(DependencyNode): def __init__(self, filename, reldir): DependencyNode.__init__(self, filename) self.reldir = reldir class SWIGSource(SourceFile): def __init__(self, filename): SourceFile.__init__(self, filename, build_path_dirname(filename)) lang_abbrev = { 'python' : 'py', 'perl' : 'pl', 'ruby' : 'rb', } lang_full_name = { 'python' : 'Python', 'perl' : 'Perl', 'ruby' : 'Ruby', } lang_utillib_suffix = { 'python' : 'py', 'perl' : 'perl', 'ruby' : 'ruby', } class Target(DependencyNode): "A build target is a node in our dependency graph." def __init__(self, name, options, gen_obj): self.name = name self.gen_obj = gen_obj self.desc = options.get('description') self.when = options.get('when') self.path = options.get('path', '') self.add_deps = options.get('add-deps', '') self.add_install_deps = options.get('add-install-deps', '') self.msvc_name = options.get('msvc-name') # override project name def add_dependencies(self): # subclasses should override to provide behavior, as appropriate raise NotImplementedError class Section: """Represents an individual section of build.conf The Section class is sort of a factory class which is responsible for creating and keeping track of Target instances associated with a section of the configuration file. By default it only allows one Target per section, but subclasses may create multiple Targets. """ def __init__(self, target_class, name, options, gen_obj): self.target_class = target_class self.name = name self.options = options self.gen_obj = gen_obj def create_targets(self): """Create target instances""" self.target = self.target_class(self.name, self.options, self.gen_obj) self.target.add_dependencies() def get_targets(self): """Return list of target instances associated with this section""" return [self.target] def get_dep_targets(self, target): """Return list of targets from this section that "target" depends on""" return [self.target] class TargetLinked(Target): "The target is linked (by libtool) against other libraries." def __init__(self, name, options, gen_obj): Target.__init__(self, name, options, gen_obj) self.install = options.get('install') self.compile_cmd = options.get('compile-cmd') self.sources = options.get('sources', '*.c *.cpp') self.link_cmd = options.get('link-cmd', '$(LINK_LIB)') self.external_lib = options.get('external-lib') self.external_project = options.get('external-project') self.msvc_libs = options.get('msvc-libs', '').split() self.msvc_delayload_targets = [] def add_dependencies(self): if self.external_lib or self.external_project: if self.external_project: self.gen_obj.projects.append(self) return # the specified install area depends upon this target self.gen_obj.graph.add(DT_INSTALL, self.install, self) sources = sorted(_collect_paths(self.sources, self.path)) for src, reldir in sources: if glob.glob(src): if src[-2:] == '.c': objname = src[:-2] + self.objext elif src[-3:] == '.cc': objname = src[:-3] + self.objext elif src[-4:] == '.cpp': objname = src[:-4] + self.objext else: raise GenError('ERROR: unknown file extension on ' + src) ofile = ObjectFile(objname, self.compile_cmd, self.when) # object depends upon source self.gen_obj.graph.add(DT_OBJECT, ofile, SourceFile(src, reldir)) # target (a linked item) depends upon object self.gen_obj.graph.add(DT_LINK, self.name, ofile) # collect all the paths where stuff might get built ### we should collect this from the dependency nodes rather than ### the sources. "what dir are you going to put yourself into?" self.gen_obj.target_dirs.append(self.path) for pattern in self.sources.split(): dirname = build_path_dirname(pattern) if dirname: self.gen_obj.target_dirs.append(build_path_join(self.path, dirname)) class TargetExe(TargetLinked): def __init__(self, name, options, gen_obj): TargetLinked.__init__(self, name, options, gen_obj) if not (self.external_lib or self.external_project): extmap = self.gen_obj._extension_map self.objext = extmap['exe', 'object'] self.filename = build_path_join(self.path, name + extmap['exe', 'target']) self.link_cmd = '$(LINK)' self.manpages = options.get('manpages', '') self.testing = options.get('testing') self.msvc_force_static = options.get('msvc-force-static') == 'yes' def add_dependencies(self): TargetLinked.add_dependencies(self) # collect test programs if 'svnauthz' in self.name: # special case self.gen_obj.test_deps.append(self.filename) self.gen_obj.test_helpers.append(self.filename) elif self.install == 'test': self.gen_obj.test_deps.append(self.filename) if self.testing != 'skip': self.gen_obj.test_progs.append(self.filename) else: self.gen_obj.test_helpers.append(self.filename) elif self.install == 'bdb-test': self.gen_obj.bdb_test_deps.append(self.filename) if self.testing != 'skip': self.gen_obj.bdb_test_progs.append(self.filename) self.gen_obj.manpages.extend(self.manpages.split()) class TargetScript(Target): def add_dependencies(self): # we don't need to "compile" the sources, so there are no dependencies # to add here, except to get the script installed in the proper area. # note that the script might itself be generated, but that isn't a # concern here. self.gen_obj.graph.add(DT_INSTALL, self.install, self) class TargetLib(TargetLinked): def __init__(self, name, options, gen_obj): TargetLinked.__init__(self, name, options, gen_obj) if not (self.external_lib or self.external_project): extmap = gen_obj._extension_map self.objext = extmap['lib', 'object'] # the target file is the name, version, and appropriate extension tfile = '%s-%s%s' % (name, gen_obj.version, extmap['lib', 'target']) self.filename = build_path_join(self.path, tfile) # Is a library referencing symbols which are undefined at link time. self.undefined_lib_symbols = options.get('undefined-lib-symbols') == 'yes' self.link_cmd = options.get('link-cmd', '$(LINK_LIB)') self.msvc_static = options.get('msvc-static') == 'yes' # is a static lib self.msvc_delayload = options.get('msvc-delayload') == 'yes' # Delay dll load self.msvc_fake = options.get('msvc-fake') == 'yes' # has fake target self.msvc_export = options.get('msvc-export', '').split() def disable_shared(self): "tries to disable building as a shared library," self.msvc_static = True class TargetApacheMod(TargetLib): def __init__(self, name, options, gen_obj): TargetLib.__init__(self, name, options, gen_obj) tfile = name + self.gen_obj._extension_map['lib', 'target'] self.filename = build_path_join(self.path, tfile) # we have a custom linking rule ### hmm. this is Makefile-specific self.compile_cmd = '$(COMPILE_APACHE_MOD)' self.link_cmd = '$(LINK_APACHE_MOD)' class TargetSharedOnlyLib(TargetLib): def __init__(self, name, options, gen_obj): TargetLib.__init__(self, name, options, gen_obj) self.compile_cmd = '$(COMPILE_SHARED_ONLY_LIB)' self.link_cmd = '$(LINK_SHARED_ONLY_LIB)' class TargetSharedOnlyCxxLib(TargetLib): def __init__(self, name, options, gen_obj): TargetLib.__init__(self, name, options, gen_obj) self.compile_cmd = '$(COMPILE_SHARED_ONLY_CXX_LIB)' self.link_cmd = '$(LINK_SHARED_ONLY_CXX_LIB)' class TargetRaModule(TargetLib): pass class TargetFsModule(TargetLib): pass class TargetDoc(Target): pass class TargetI18N(Target): "The target is a collection of .po files to be compiled by msgfmt." def __init__(self, name, options, gen_obj): Target.__init__(self, name, options, gen_obj) self.install = options.get('install') self.sources = options.get('sources') # Let the Makefile determine this via .SUFFIXES self.compile_cmd = None self.objext = '.mo' self.external_project = options.get('external-project') def add_dependencies(self): self.gen_obj.graph.add(DT_INSTALL, self.install, self) sources = sorted(_collect_paths(self.sources or '*.po', self.path)) for src, reldir in sources: if src[-3:] == '.po': objname = src[:-3] + self.objext else: raise GenError('ERROR: unknown file extension on ' + src) ofile = ObjectFile(objname, self.compile_cmd, self.when) # object depends upon source self.gen_obj.graph.add(DT_OBJECT, ofile, SourceFile(src, reldir)) # target depends upon object self.gen_obj.graph.add(DT_LINK, self.name, ofile) # Add us to the list of target dirs, so we're created in mkdir-init. self.gen_obj.target_dirs.append(self.path) class TargetSWIG(TargetLib): def __init__(self, name, options, gen_obj, lang): TargetLib.__init__(self, name, options, gen_obj) self.lang = lang self.desc = self.desc + ' for ' + lang_full_name[lang] ### hmm. this is Makefile-specific self.link_cmd = '$(LINK_%s_WRAPPER)' % lang_abbrev[lang].upper() def add_dependencies(self): # Look in source directory for dependencies self.gen_obj.target_dirs.append(self.path) sources = _collect_paths(self.sources, self.path) assert len(sources) == 1 ### simple assertions for now # get path to SWIG .i file ipath = sources[0][0] iname = build_path_basename(ipath) assert iname[-2:] == '.i' cname = iname[:-2] + '.c' oname = iname[:-2] + self.gen_obj._extension_map['pyd', 'object'] # Extract SWIG module name from .i file name module_name = iname[:4] != 'svn_' and iname[:-2] or iname[4:-2] lib_extension = self.gen_obj._extension_map['lib', 'target'] if self.lang == "python": lib_extension = self.gen_obj._extension_map['pyd', 'target'] lib_filename = '_' + module_name + lib_extension elif self.lang == "ruby": lib_extension = self.gen_obj._extension_map['so', 'target'] lib_filename = module_name + lib_extension elif self.lang == "perl": lib_filename = '_' + module_name.capitalize() + lib_extension else: lib_filename = module_name + lib_extension self.name = self.lang + '_' + module_name self.path = build_path_join(self.path, self.lang) if self.lang == "perl": self.path = build_path_join(self.path, "native") self.filename = build_path_join(self.path, lib_filename) ifile = SWIGSource(ipath) cfile = SWIGObject(build_path_join(self.path, cname), self.lang, self.gen_obj.release_mode) ofile = SWIGObject(build_path_join(self.path, oname), self.lang, self.gen_obj.release_mode) # the .c file depends upon the .i file self.gen_obj.graph.add(DT_SWIG_C, cfile, ifile) # the object depends upon the .c file self.gen_obj.graph.add(DT_OBJECT, ofile, cfile) # the library depends upon the object self.gen_obj.graph.add(DT_LINK, self.name, ofile) # the specified install area depends upon the library self.gen_obj.graph.add(DT_INSTALL, 'swig-' + lang_abbrev[self.lang], self) class Section(TargetLib.Section): def create_targets(self): self.targets = { } for lang in self.gen_obj.swig_lang: target = self.target_class(self.name, self.options, self.gen_obj, lang) target.add_dependencies() self.targets[lang] = target def get_targets(self): return list(self.targets.values()) def get_dep_targets(self, target): target = self.targets.get(target.lang, None) return target and [target] or [ ] class TargetSWIGLib(TargetLib): def __init__(self, name, options, gen_obj): TargetLib.__init__(self, name, options, gen_obj) self.lang = options.get('lang') class Section(TargetLib.Section): def get_dep_targets(self, target): if target.lang == self.target.lang: return [ self.target ] return [ ] def disable_shared(self): "disables building shared libraries" return # Explicit NO-OP class TargetProject(Target): def __init__(self, name, options, gen_obj): Target.__init__(self, name, options, gen_obj) self.cmd = options.get('cmd') self.release = options.get('release') self.debug = options.get('debug') def add_dependencies(self): self.gen_obj.projects.append(self) class TargetSWIGProject(TargetProject): def __init__(self, name, options, gen_obj): TargetProject.__init__(self, name, options, gen_obj) self.lang = options.get('lang') class TargetJava(TargetLinked): def __init__(self, name, options, gen_obj): TargetLinked.__init__(self, name, options, gen_obj) self.link_cmd = options.get('link-cmd') self.package = options.get('package') self.jar = options.get('jar') self.deps = [ ] self.objext = '.class' self.headers = options.get('headers') self.classes = options.get('classes') self.native = options.get('native', '') self.output_dir = self.classes self.headers_dir = self.headers def add_dependencies(self): sources = _collect_paths(self.sources, self.path) native = _collect_paths(self.native, self.path) class_pkg_list = self.package.split('.') sourcepath = build_path_split(self.path)[:-len(class_pkg_list)] sourcepath = build_path_join(*sourcepath) for src, reldir in sources: if src[-5:] != '.java': raise GenError('ERROR: unknown file extension on ' + src) sfile = SourceFile(src, reldir) sfile.sourcepath = sourcepath class_name = build_path_basename(src[:-5]) class_pkg = build_path_join(*class_pkg_list) class_file = ObjectFile(build_path_join(self.classes, class_pkg, class_name + self.objext), self.compile_cmd, self.when) class_file.source_generated = 1 class_file.class_name = class_name self.gen_obj.graph.add(DT_OBJECT, class_file, sfile) self.gen_obj.graph.add(DT_LINK, self.name, class_file) self.deps.append(class_file) if (src, reldir) in native: class_header = build_path_join(self.headers, class_name + '.h') class_header_win = build_path_join(self.headers, self.package.replace(".", "_") + "_" + class_name + '.h') hfile = HeaderFile(class_header, self.package + '.' + class_name, self.compile_cmd) hfile.filename_win = class_header_win hfile.source_generated = 1 self.gen_obj.graph.add(DT_OBJECT, hfile, sfile) self.deps.append(hfile) # target (a linked item) depends upon object self.gen_obj.graph.add(DT_LINK, self.name, hfile) # collect all the paths where stuff might get built ### we should collect this from the dependency nodes rather than ### the sources. "what dir are you going to put yourself into?" self.gen_obj.target_dirs.append(self.path) self.gen_obj.target_dirs.append(self.classes) if self.headers: self.gen_obj.target_dirs.append(self.headers) for pattern in self.sources.split(): dirname = build_path_dirname(pattern) if dirname: self.gen_obj.target_dirs.append(build_path_join(self.path, dirname)) self.gen_obj.graph.add(DT_INSTALL, self.name, self) class TargetSQLHeader(Target): def __init__(self, name, options, gen_obj): Target.__init__(self, name, options, gen_obj) self.sources = options.get('sources') _re_sql_include = re.compile('-- *include: *([-a-z]+)') def add_dependencies(self): sources = _collect_paths(self.sources, self.path) assert len(sources) == 1 # support for just one source, for now source, reldir = sources[0] assert reldir == '' # no support for reldir right now assert source.endswith('.sql') output = source[:-4] + '.h' self.gen_obj.graph.add(DT_SQLHDR, output, source) for line in fileinput.input(source): match = self._re_sql_include.match(line) if not match: continue file = match.group(1) self.gen_obj.graph.add(DT_SQLHDR, output, os.path.join(os.path.dirname(source), file + '.sql')) _build_types = { 'exe' : TargetExe, 'script' : TargetScript, 'lib' : TargetLib, 'doc' : TargetDoc, 'swig' : TargetSWIG, 'project' : TargetProject, 'swig_lib' : TargetSWIGLib, 'swig_project' : TargetSWIGProject, 'ra-module': TargetRaModule, 'fs-module': TargetFsModule, 'apache-mod': TargetApacheMod, 'shared-only-lib': TargetSharedOnlyLib, 'shared-only-cxx-lib': TargetSharedOnlyCxxLib, 'java' : TargetJava, 'i18n' : TargetI18N, 'sql-header' : TargetSQLHeader, } class GenError(Exception): pass # Path Handling Functions # # Build paths specified in build.conf are assumed to be always separated # by forward slashes, regardless of the current running os. # # Native paths are paths separated by os.sep. def native_path(path): """Convert a build path to a native path""" return path.replace('/', os.sep) def build_path(path): """Convert a native path to a build path""" path = path.replace(os.sep, '/') if os.altsep: path = path.replace(os.altsep, '/') return path def build_path_join(*path_parts): """Join path components into a build path""" return '/'.join(path_parts) def build_path_split(path): """Return list of components in a build path""" return path.split('/') def build_path_splitfile(path): """Return the filename and directory portions of a file path""" pos = path.rfind('/') if pos > 0: return path[:pos], path[pos+1:] elif pos == 0: return path[0], path[1:] else: return "", path def build_path_dirname(path): """Return the directory portion of a file path""" return build_path_splitfile(path)[0] def build_path_basename(path): """Return the filename portion of a file path""" return build_path_splitfile(path)[1] def build_path_retreat(path): "Given a relative directory, return ../ paths to retreat to the origin." return ".." + "/.." * path.count('/') def build_path_strip(path, files): "Strip the given path from each file." l = len(path) result = [ ] for file in files: if len(file) > l and file[:l] == path and file[l] == '/': result.append(file[l+1:]) else: result.append(file) return result def _collect_paths(pats, path=None): """Find files matching a space separated list of globs pats (string) is the list of glob patterns path (string), if specified, is a path that will be prepended to each glob pattern before it is evaluated If path is None the return value is a list of filenames, otherwise the return value is a list of 2-tuples. The first element in each tuple is a matching filename and the second element is the portion of the glob pattern which matched the file before its last forward slash (/) If no files are found matching a pattern, then include the pattern itself as a filename in the results. """ result = [ ] for base_pat in pats.split(): if path: pattern = build_path_join(path, base_pat) else: pattern = base_pat files = sorted(glob.glob(native_path(pattern))) or [pattern] if path is None: # just append the names to the result list for file in files: result.append(build_path(file)) else: # if we have paths, then we need to record how each source is located # relative to the specified path reldir = build_path_dirname(base_pat) for file in files: result.append((build_path(file), reldir)) return result _re_public_include = re.compile(r'^subversion/include/(\w+)\.h$') def _is_public_include(fname): return _re_public_include.match(build_path(fname)) def _swig_include_wrapper(fname): return native_path(_re_public_include.sub( r"subversion/bindings/swig/proxy/\1_h.swg", build_path(fname))) def _path_endswith(path, subpath): """Check if SUBPATH is a true path suffix of PATH. """ path_len = len(path) subpath_len = len(subpath) return (subpath_len > 0 and path_len >= subpath_len and path[-subpath_len:] == subpath and (path_len == subpath_len or (subpath[0] == os.sep and path[-subpath_len] == os.sep) or path[-subpath_len - 1] == os.sep)) class IncludeDependencyInfo: """Finds all dependencies between a named set of headers, and computes closure, so that individual C and SWIG source files can then be scanned, and the stored dependency data used to return all directly and indirectly referenced headers. Note that where SWIG is concerned, there are two different kinds of include: (1) those that include files in SWIG processing, and so matter to the generation of .c files. (These are %include, %import). (2) those that include references to C headers in the generated output, and so are not required at .c generation, only at .o generation. (These are %{ #include ... %}). This class works exclusively in native-style paths.""" def __init__(self, filenames, fnames_nonexist): """Operation of an IncludeDependencyInfo instance is restricted to a 'domain' - a set of header files which are considered interesting when following and reporting dependencies. This is done to avoid creating any dependencies on system header files. The domain is defined by three factors: (1) FILENAMES is a list of headers which are in the domain, and should be scanned to discover how they inter-relate. (2) FNAMES_NONEXIST is a list of headers which are in the domain, but will be created by the build process, and so are not available to be scanned - they will be assumed not to depend on any other interesting headers. (3) Files in subversion/bindings/swig/proxy/, which are based autogenerated based on files in subversion/include/, will be added to the domain when a file in subversion/include/ is processed, and dependencies will be deduced by special-case logic. """ # This defines the domain (i.e. set of files) in which dependencies are # being located. Its structure is: # { 'basename.h': [ 'path/to/something/named/basename.h', # 'path/to/another/named/basename.h', ] } self._domain = {} for fname in filenames + fnames_nonexist: bname = os.path.basename(fname) self._domain.setdefault(bname, []).append(fname) if _is_public_include(fname): swig_fname = _swig_include_wrapper(fname) swig_bname = os.path.basename(swig_fname) self._domain.setdefault(swig_bname, []).append(swig_fname) # This data structure is: # { 'full/path/to/header.h': { 'full/path/to/dependency.h': TYPECODE, } } # TYPECODE is '#', denoting a C include, or '%' denoting a SWIG include. self._deps = {} for fname in filenames: self._deps[fname] = self._scan_for_includes(fname) if _is_public_include(fname): hdrs = { self._domain["proxy.swg"][0]: '%', self._domain["apr.swg"][0]: '%', fname: '%' } for h in self._deps[fname].keys(): if (_is_public_include(h) or h == os.path.join('subversion', 'include', 'private', 'svn_debug.h')): hdrs[_swig_include_wrapper(h)] = '%' else: raise RuntimeError("Public include '%s' depends on '%s', " \ "which is not a public include! What's going on?" % (fname, h)) swig_fname = _swig_include_wrapper(fname) swig_bname = os.path.basename(swig_fname) self._deps[swig_fname] = hdrs for fname in fnames_nonexist: self._deps[fname] = {} # Keep recomputing closures until we see no more changes while True: changes = 0 for fname in self._deps.keys(): changes = self._include_closure(self._deps[fname]) or changes if not changes: break def query_swig(self, fname): """Scan the C or SWIG file FNAME, and return the full paths of each include file that is a direct or indirect dependency, as a 2-tuple: (C_INCLUDES, SWIG_INCLUDES).""" if fname in self._deps: hdrs = self._deps[fname] else: hdrs = self._scan_for_includes(fname) self._include_closure(hdrs) c_filenames = [] swig_filenames = [] for hdr, hdr_type in hdrs.items(): if hdr_type == '#': c_filenames.append(hdr) else: # hdr_type == '%' swig_filenames.append(hdr) # Be independent of hash ordering c_filenames.sort() swig_filenames.sort() return (c_filenames, swig_filenames) def query(self, fname): """Same as SELF.QUERY_SWIG(FNAME), but assert that there are no SWIG includes, and return only C includes as a single list.""" c_includes, swig_includes = self.query_swig(fname) assert len(swig_includes) == 0 return c_includes def _include_closure(self, hdrs): """Mutate the passed dictionary HDRS, by performing a single pass through the listed headers, adding the headers on which the first group of headers depend, if not already present. HDRS is of the form { 'path/to/header.h': TYPECODE, } Return a boolean indicating whether any changes were made.""" items = list(hdrs.items()) for this_hdr, this_type in items: for dependency_hdr, dependency_type in self._deps[this_hdr].items(): self._upd_dep_hash(hdrs, dependency_hdr, dependency_type) return (len(items) != len(hdrs)) def _upd_dep_hash(self, hash, hdr, type): """Mutate HASH (a data structure of the form { 'path/to/header.h': TYPECODE, } ) to include additional info of a dependency of type TYPE on the file HDR.""" # '%' (SWIG, .c: .i) has precedence over '#' (C, .o: .c) if hash.get(hdr) != '%': hash[hdr] = type _re_include = \ re.compile(r'^\s*([#%])\s*(?:include|import)\s*([<"])?([^<">;\s]+)') def _scan_for_includes(self, fname): """Scan C source file FNAME and return the basenames of any headers which are directly included, and within the set defined when this IncludeDependencyProcessor was initialized. Return a dictionary with included full file names as keys and None as values.""" hdrs = { } for line in fileinput.input(fname): match = self._re_include.match(line) if not match: continue include_param = native_path(match.group(3)) type_code = match.group(1) direct_possibility_fname = os.path.normpath(os.path.join( os.path.dirname(fname), include_param)) domain_fnames = self._domain.get(os.path.basename(include_param), []) if os.sep.join(['libsvn_subr', 'error.c']) in fname \ and 'errorcode.inc' == include_param: continue # generated by GeneratorBase.write_errno_table if os.sep.join(['libsvn_subr', 'cmdline.c']) in fname \ and 'config_keys.inc' == include_param: continue # generated by GeneratorBase.write_config_keys elif direct_possibility_fname in domain_fnames: self._upd_dep_hash(hdrs, direct_possibility_fname, type_code) elif (len(domain_fnames) == 1 and (include_param.find(os.sep) == -1 or _path_endswith(domain_fnames[0], include_param))): self._upd_dep_hash(hdrs, domain_fnames[0], type_code) else: # None found if include_param.find(os.sep) == -1 and len(domain_fnames) > 1: _error( "Unable to determine which file is being included\n" " Include Parameter: '%s'\n" " Including File: '%s'\n" " Direct possibility: '%s'\n" " Other possibilities: %s\n" % (include_param, fname, direct_possibility_fname, domain_fnames)) if match.group(2) == '"': _warning('"%s" header not found, file %s' % (include_param, fname)) continue if match.group(2) == '<': _warning('<%s> header *found*, file %s' % (include_param, fname)) # The above warnings help to avoid the following problems: # - If header is uses the correct <> or "" convention, then the warnings # reveal if the build generator does/does not make dependencies for it # when it should not/should - e.g. might reveal changes needed to # build.conf. # ...and... # - If the generator is correct, them the warnings reveal incorrect use # of <>/"" convention. return hdrs class FileInfo: def __init__(self, filename, when, target=None): self.filename = filename self.when = when self.target = target def _sorted_files(graph, area): "Given a list of targets, sort them based on their dependencies." # we're going to just go with a naive algorithm here. these lists are # going to be so short, that we can use O(n^2) or whatever this is. inst_targets = graph.get_sources(DT_INSTALL, area) # first we need our own copy of the target list since we're going to # munge it. targets = inst_targets[:] # the output list of the targets' files files = [ ] # loop while we have targets remaining: while targets: # find a target that has no dependencies in our current targets list. for t in targets: s = graph.get_sources(DT_LINK, t.name, Target) \ + graph.get_sources(DT_NONLIB, t.name, Target) for d in s: if d in targets: break else: # no dependencies found in the targets list. this is a good "base" # to add to the files list now. if isinstance(t, TargetJava): # Java targets have no filename, and we just ignore them. pass elif isinstance(t, TargetI18N): # I18N targets have no filename, we recurse one level deeper, and # get the filenames of their dependencies. s = graph.get_sources(DT_LINK, t.name) for d in s: if d not in targets: files.append(FileInfo(d.filename, d.when, d)) else: files.append(FileInfo(t.filename, t.when, t)) # don't consider this target any more targets.remove(t) # break out of search through targets break else: # we went through the entire target list and everything had at least # one dependency on another target. thus, we have a circular dependency # tree. somebody messed up the .conf file, or the app truly does have # a loop (and if so, they're screwed; libtool can't relink a lib at # install time if the dependent libs haven't been installed yet) raise CircularDependencies() return files class CircularDependencies(Exception): pass def unique(seq): "Eliminate duplicates from a sequence" list = [ ] dupes = { } for e in seq: if e not in dupes: dupes[e] = None list.append(e) return list ### End of file.