| #!/usr/bin/python |
| # |
| # Copyright (c) 2017 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Creates a GN include file for building FFmpeg from source. |
| |
| The way this works is a bit silly but it's easier than reverse engineering |
| FFmpeg's configure scripts and Makefiles and manually maintaining fuchsia |
| build files. It scans through build directories for object files then does a |
| reverse lookup against the FFmpeg source tree to find the corresponding C or |
| assembly file. |
| |
| Running build_ffmpeg.py for all (both) architectures is required prior to |
| running this script. |
| |
| Once you've built all architectures you may run this script. |
| """ |
| |
| import collections |
| import copy |
| import datetime |
| from enum import enum |
| import fnmatch |
| import credits_updater |
| import itertools |
| import optparse |
| import os |
| import re |
| import shutil |
| import string |
| import subprocess |
| import sys |
| |
| COPYRIGHT = """# Copyright %d The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # NOTE: this file is autogenerated by ffmpeg/fuchsia/scripts/generate_gn.py |
| |
| """ % (datetime.datetime.now().year) |
| |
| GN_HEADER = """import("//build/config/arm.gni") |
| import("ffmpeg_options.gni") |
| |
| # Declare empty versions of each variable for easier +=ing later. |
| ffmpeg_c_sources = [] |
| ffmpeg_gas_sources = [] |
| ffmpeg_yasm_sources = [] |
| |
| """ |
| GN_CONDITION_BEGIN = """if (%s) { |
| """ |
| GN_CONDITION_END = """} |
| |
| """ |
| GN_C_SOURCES_BEGIN = """ffmpeg_c_sources += [ |
| """ |
| GN_GAS_SOURCES_BEGIN = """ffmpeg_gas_sources += [ |
| """ |
| GN_YASM_SOURCES_BEGIN = """ffmpeg_yasm_sources += [ |
| """ |
| GN_SOURCE_ITEM = """ "%s", |
| """ |
| GN_SOURCE_END = """] |
| """ |
| |
| # Controls conditional stanza generation. |
| Attr = enum('ARCHITECTURE', 'PROFILE') |
| SUPPORT_MATRIX = { |
| Attr.ARCHITECTURE: set(['x64', 'arm64']), |
| Attr.PROFILE: set(['default', 'max']), |
| } |
| |
| |
| def NormalizeFilename(name): |
| """Removes leading path separators in an attempt to normalize paths.""" |
| return string.lstrip(name, os.sep) |
| |
| |
| def CleanObjectFiles(object_files): |
| """Removes unneeded object files due to linker errors, binary size, etc... |
| |
| Args: |
| object_files: List of object files that needs cleaning. |
| """ |
| blacklist = [ |
| 'libavcodec/inverse.o', # Includes libavutil/inverse.c |
| 'libavcodec/file_open.o', # Includes libavutil/file_open.c |
| 'libavcodec/log2_tab.o', # Includes libavutil/log2_tab.c |
| 'libavformat/golomb_tab.o', # Includes libavcodec/golomb.c |
| 'libavformat/log2_tab.o', # Includes libavutil/log2_tab.c |
| 'libavformat/file_open.o', # Includes libavutil/file_open.c |
| |
| # The following files are removed to trim down on binary size. |
| # TODO(ihf): Warning, it is *easy* right now to remove more files |
| # than is healthy and end up with a library that the linker does |
| # not complain about but that can't be loaded. Add some verification! |
| 'libavcodec/audioconvert.o', |
| 'libavcodec/resample.o', |
| 'libavcodec/resample2.o', |
| 'libavcodec/x86/dnxhd_mmx.o', |
| 'libavformat/sdp.o', |
| 'libavutil/adler32.o', |
| 'libavutil/audio_fifo.o', |
| 'libavutil/blowfish.o', |
| 'libavutil/cast5.o', |
| 'libavutil/des.o', |
| 'libavutil/file.o', |
| 'libavutil/hash.o', |
| 'libavutil/hmac.o', |
| 'libavutil/lls.o', |
| 'libavutil/murmur3.o', |
| 'libavutil/rc4.o', |
| 'libavutil/ripemd.o', |
| 'libavutil/sha512.o', |
| 'libavutil/tree.o', |
| 'libavutil/xtea.o', |
| 'libavutil/xga_font_data.o', |
| ] |
| for name in blacklist: |
| name = name.replace('/', os.sep) |
| if name in object_files: |
| object_files.remove(name) |
| return object_files |
| |
| |
| def IsAssemblyFile(f): |
| _, ext = os.path.splitext(f) |
| return ext in ['.S', '.asm'] |
| |
| |
| def IsGasFile(f): |
| _, ext = os.path.splitext(f) |
| return ext in ['.S'] |
| |
| |
| def IsYasmFile(f): |
| _, ext = os.path.splitext(f) |
| return ext in ['.asm'] |
| |
| |
| def IsCFile(f): |
| _, ext = os.path.splitext(f) |
| return ext in ['.c'] |
| |
| |
| def IsSourceFile(f): |
| return IsAssemblyFile(f) or IsCFile(f) |
| |
| |
| def GetSourceFiles(source_dir): |
| """Returns a list of source files for the given source directory. |
| |
| Args: |
| source_dir: Path to build a source mapping for. |
| |
| Returns: |
| A python list of source file paths. |
| """ |
| |
| def IsSourceDir(d): |
| return d != '.git' |
| |
| source_files = [] |
| for root, dirs, files in os.walk(source_dir): |
| dirs = filter(IsSourceDir, dirs) |
| files = filter(IsSourceFile, files) |
| |
| # Strip leading source_dir from root. |
| root = root[len(source_dir):] |
| source_files.extend([NormalizeFilename(os.path.join(root, name)) for name in |
| files]) |
| return source_files |
| |
| |
| def GetObjectFiles(build_dir): |
| """Returns a list of object files for the given build directory. |
| |
| Args: |
| build_dir: Path to build an object file list for. |
| |
| Returns: |
| A python list of object files paths. |
| """ |
| object_files = [] |
| for root, _, files in os.walk(build_dir): |
| # Strip leading build_dir from root. |
| root = root[len(build_dir):] |
| |
| for name in files: |
| _, ext = os.path.splitext(name) |
| if ext == '.o': |
| name = NormalizeFilename(os.path.join(root, name)) |
| object_files.append(name) |
| CleanObjectFiles(object_files) |
| return object_files |
| |
| |
| def GetObjectToSourceMapping(source_files): |
| """Returns a map of object file paths to source file paths. |
| |
| Args: |
| source_files: List of source file paths. |
| |
| Returns: |
| Map with object file paths as keys and source file paths as values. |
| """ |
| object_to_sources = {} |
| for name in source_files: |
| basename, _ = os.path.splitext(name) |
| key = basename + '.o' |
| object_to_sources[key] = name |
| return object_to_sources |
| |
| |
| def GetSourceFileSet(object_to_sources, object_files): |
| """Determines set of source files given object files. |
| |
| Args: |
| object_to_sources: A dictionary of object to source file paths. |
| object_files: A list of object file paths. |
| |
| Returns: |
| A python set of source files required to build said objects. |
| """ |
| source_set = set() |
| for name in object_files: |
| # Intentially raise a KeyError if lookup fails since something is messed |
| # up with our source and object lists. |
| source_set.add(object_to_sources[name]) |
| return source_set |
| |
| SourceListCondition = collections.namedtuple('SourceListCondition', |
| [Attr.ARCHITECTURE, |
| Attr.PROFILE]) |
| |
| |
| class SourceSet(object): |
| """A SourceSet represents a set of source files that are built on each of the |
| given set of SourceListConditions. |
| """ |
| |
| def __init__(self, sources, conditions): |
| """Creates a SourceSet. |
| |
| Args: |
| sources: a python set of source files |
| conditions: a python set of SourceListConditions where the given sources |
| are to be used. |
| """ |
| self.sources = sources |
| self.conditions = conditions |
| |
| def __repr__(self): |
| return '{%s, %s}' % (self.sources, self.conditions) |
| |
| def __eq__(self, other): |
| return (self.sources == other.sources and |
| self.conditions == other.conditions) |
| |
| def __hash__(self): |
| return hash((frozenset(self.sources), frozenset(self.conditions))) |
| |
| def Intersect(self, other): |
| """Return a new SourceSet containing the set of source files common to both |
| this and the other SourceSet. |
| |
| The resulting SourceSet represents the union of the architectures and |
| profiles of this and the other SourceSet. |
| """ |
| return SourceSet(self.sources & other.sources, |
| self.conditions | other.conditions) |
| |
| def Difference(self, other): |
| """Return a new SourceSet containing the set of source files not present in |
| the other SourceSet. |
| |
| The resulting SourceSet represents the intersection of the |
| SourceListConditions from this and the other SourceSet. |
| """ |
| return SourceSet(self.sources - other.sources, |
| self.conditions & other.conditions) |
| |
| def IsEmpty(self): |
| """An empty SourceSet is defined as containing no source files or no |
| conditions (i.e., a set of files that aren't built on anywhere). |
| """ |
| return (len(self.sources) == 0 or len(self.conditions) == 0) |
| |
| def GenerateGnStanza(self): |
| """Generates a gn conditional stanza representing this source set. |
| """ |
| |
| conjunctions = [] |
| for condition in self.conditions: |
| if condition.ARCHITECTURE == '*': |
| arch_condition = None |
| else: |
| arch_condition = 'current_cpu == "%s"' % condition.ARCHITECTURE |
| |
| # Profile conditions look like: |
| # profile == "default" |
| if condition.PROFILE == '*': |
| profile_condition = None |
| else: |
| profile_condition = 'ffmpeg_profile == "%s"' % condition.PROFILE |
| |
| conjunction_parts = filter( |
| None, [arch_condition, profile_condition]) |
| conjunctions.append(' && '.join(conjunction_parts)) |
| |
| # If there is more that one clause, wrap various conditions in parens |
| # before joining. |
| if len(conjunctions) > 1: |
| conjunctions = ['(%s)' % x for x in conjunctions] |
| |
| # Sort conjunctions to make order deterministic. |
| joined_conjuctions = ' || '.join(sorted(conjunctions)) |
| |
| stanza = '' |
| # Output a conditional wrapper around stanzas if necessary. |
| if joined_conjuctions: |
| stanza += GN_CONDITION_BEGIN % joined_conjuctions |
| |
| def indent(s): |
| return ' %s' % s |
| else: |
| def indent(s): |
| return s |
| |
| sources = sorted(n.replace('\\', '/') for n in self.sources) |
| |
| # Write out all C sources. |
| c_sources = filter(IsCFile, sources) |
| if c_sources: |
| stanza += indent(GN_C_SOURCES_BEGIN) |
| for name in c_sources: |
| stanza += indent(GN_SOURCE_ITEM % (name)) |
| stanza += indent(GN_SOURCE_END) |
| |
| # Write out all assembly sources. |
| gas_sources = filter(IsGasFile, sources) |
| if gas_sources: |
| stanza += indent(GN_GAS_SOURCES_BEGIN) |
| for name in gas_sources: |
| stanza += indent(GN_SOURCE_ITEM % (name)) |
| stanza += indent(GN_SOURCE_END) |
| |
| # Write out all assembly sources. |
| yasm_sources = filter(IsYasmFile, sources) |
| if yasm_sources: |
| stanza += indent(GN_YASM_SOURCES_BEGIN) |
| for name in yasm_sources: |
| stanza += indent(GN_SOURCE_ITEM % (name)) |
| stanza += indent(GN_SOURCE_END) |
| |
| # Close the conditional if necessary. |
| if joined_conjuctions: |
| stanza += GN_CONDITION_END |
| else: |
| stanza += '\n' # Makeup the spacing for the remove conditional. |
| return stanza |
| |
| |
| def CreatePairwiseDisjointSets(sets): |
| """Given a list of SourceSet objects, returns the pairwise disjoint sets. |
| |
| NOTE: This isn't the most efficient algorithm, but given how infrequent we |
| need to run this and how small the input size is we'll leave it as is. |
| """ |
| |
| disjoint_sets = list(sets) |
| |
| new_sets = True |
| while new_sets: |
| new_sets = False |
| for pair in itertools.combinations(disjoint_sets, 2): |
| intersection = pair[0].Intersect(pair[1]) |
| |
| # Both pairs are already disjoint, nothing to do. |
| if intersection.IsEmpty(): |
| continue |
| |
| # Add the resulting intersection set. |
| new_sets = True |
| disjoint_sets.append(intersection) |
| |
| # Calculate the resulting differences for this pair of sets. |
| # |
| # If the differences are an empty set, remove them from the list of sets, |
| # otherwise update the set itself. |
| for p in pair: |
| i = disjoint_sets.index(p) |
| difference = p.Difference(intersection) |
| if difference.IsEmpty(): |
| del disjoint_sets[i] |
| else: |
| disjoint_sets[i] = difference |
| |
| # Restart the calculation since the list of disjoint sets has changed. |
| break |
| |
| return disjoint_sets |
| |
| |
| def GetAllMatchingConditions(conditions, condition_to_match): |
| """Given a set of conditions, find those that match the condition_to_match. |
| Matches are found when all attributes of the condition have the same value as |
| the condition_to_match, or value is accepted for wildcard attributes within |
| condition_to_match. |
| """ |
| |
| found_matches = set() |
| |
| # Check all attributes of condition for matching values. |
| def accepts_all_values(attribute): |
| return getattr(condition_to_match, attribute) == '*' |
| attributes_to_check = [a for a in Attr if not accepts_all_values(a)] |
| |
| # If all attributes allow wildcard, all conditions are considered matching |
| if not attributes_to_check: |
| return conditions |
| |
| # Check all conditions and accumulate matches. |
| for condition in conditions: |
| condition_matches = True |
| for attribute in attributes_to_check: |
| if (getattr(condition, attribute) |
| != getattr(condition_to_match, attribute)): |
| condition_matches = False |
| break |
| if condition_matches: |
| found_matches.add(condition) |
| |
| return found_matches |
| |
| def GetAttributeValuesRange(attribute, condition): |
| """Get the range of values for the given attribute considering the values |
| of all attributes in the given condition.""" |
| if getattr(condition, attribute) == '*': |
| values = copy.copy(SUPPORT_MATRIX[attribute]) |
| else: |
| values = set([getattr(condition, attribute)]) |
| |
| return values |
| |
| def GenerateConditionExpansion(condition): |
| """Expand wildcard in condition into all possible matching conditions.""" |
| architectures = GetAttributeValuesRange(Attr.ARCHITECTURE, condition) |
| profiles = GetAttributeValuesRange(Attr.PROFILE, condition) |
| return set(SourceListCondition(arch, profile) |
| for (arch, profile) |
| in itertools.product(architectures, profiles)) |
| |
| def ReduceConditionalLogic(source_set): |
| """Reduces the conditions for the given SourceSet. |
| |
| The reduction leverages what we know about the space of possible combinations, |
| finding cases where conditions span all values possible of a given attribute. |
| In such cases, these conditions can be flattened into a single condition with |
| the spanned attribute removed. |
| |
| There is room for further reduction (e.g. Quine-McCluskey), not implemented |
| at this time.""" |
| |
| ConditionReduction = collections.namedtuple('ConditionReduction', |
| 'condition, matches') |
| reduced_conditions = set() |
| |
| for condition in source_set.conditions: |
| condition_dict = condition._asdict() |
| |
| for attribute in Attr: |
| # Set attribute value to wildcard and find matching attributes. |
| original_attribute_value = condition_dict[attribute] |
| condition_dict[attribute] = '*' |
| new_condition = SourceListCondition(**condition_dict) |
| |
| # Conditions with wildcards can replace existing conditions iff the |
| # source set contains conditions covering all possible expansions |
| # of the wildcarded values. |
| matches = GetAllMatchingConditions(source_set.conditions, new_condition) |
| if matches == GenerateConditionExpansion(new_condition): |
| reduced_conditions.add(ConditionReduction(new_condition, |
| frozenset(matches))) |
| else: |
| # This wildcard won't work, restore the original value. |
| condition_dict[attribute] = original_attribute_value |
| |
| # Finally, find the most efficient reductions. Do a pairwise comparison of all |
| # reductions to de-dup and remove those that are covered by more inclusive |
| # conditions. |
| did_work = True |
| while did_work: |
| did_work = False |
| for reduction_pair in itertools.combinations(reduced_conditions, 2): |
| if reduction_pair[0].matches.issubset(reduction_pair[1].matches): |
| reduced_conditions.remove(reduction_pair[0]) |
| did_work = True |
| break |
| elif reduction_pair[1].matches.issubset(reduction_pair[0].matches): |
| reduced_conditions.remove(reduction_pair[1]) |
| did_work = True |
| break |
| |
| # Apply the reductions to the source_set. |
| for reduction in reduced_conditions: |
| source_set.conditions.difference_update(reduction.matches) |
| source_set.conditions.add(reduction.condition) |
| |
| |
| def ParseOptions(): |
| """Parses the options and terminates program if they are not sane. |
| |
| Returns: |
| The pair (optparse.OptionValues, [string]), that is the output of |
| a successful call to parser.parse_args(). |
| """ |
| parser = optparse.OptionParser( |
| usage='usage: %prog [options] DIR') |
| |
| parser.add_option('-s', |
| '--source_dir', |
| dest='source_dir', |
| default='.', |
| metavar='DIR', |
| help='FFmpeg source directory.') |
| |
| parser.add_option('-b', |
| '--build_dir', |
| dest='build_dir', |
| default='.', |
| metavar='DIR', |
| help='Build root containing build.x64, etc...') |
| |
| options, args = parser.parse_args() |
| |
| if not options.source_dir: |
| parser.error('No FFmpeg source directory specified') |
| elif not os.path.exists(options.source_dir): |
| parser.error('FFmpeg source directory does not exist') |
| |
| if not options.build_dir: |
| parser.error('No build root directory specified') |
| elif not os.path.exists(options.build_dir): |
| parser.error('FFmpeg build directory does not exist') |
| |
| return options, args |
| |
| |
| def WriteGn(fd, disjoint_sets): |
| fd.write(COPYRIGHT) |
| fd.write(GN_HEADER) |
| |
| # Generate conditional stanza for each disjoint source set. |
| for s in reversed(disjoint_sets): |
| fd.write(s.GenerateGnStanza()) |
| |
| |
| # Lists of files that are exempt from searching in GetIncludedSources. |
| IGNORED_INCLUDE_FILES = [ |
| # Fuchsia generated files |
| 'config.h', |
| os.path.join('libavcodec', 'bsf_list.c'), |
| os.path.join('libavcodec', 'codec_list.c'), |
| os.path.join('libavcodec', 'parser_list.c'), |
| os.path.join('libavformat', 'demuxer_list.c'), |
| os.path.join('libavformat', 'muxer_list.c'), |
| os.path.join('libavformat', 'protocol_list.c'), |
| os.path.join('libavutil', 'avconfig.h'), |
| os.path.join('libavutil', 'ffversion.h'), |
| |
| # Current configure values are set such that we don't include these (because |
| # of various defines) and we also don't generate them at all, so we will |
| # fail to find these because they don't exist in our repository. |
| os.path.join('libavcodec', 'aacps_tables.h'), |
| os.path.join('libavcodec', 'aacps_fixed_tables.h'), |
| os.path.join('libavcodec', 'aacsbr_tables.h'), |
| os.path.join('libavcodec', 'aac_tables.h'), |
| os.path.join('libavcodec', 'cabac_tables.h'), |
| os.path.join('libavcodec', 'cbrt_tables.h'), |
| os.path.join('libavcodec', 'cbrt_fixed_tables.h'), |
| os.path.join('libavcodec', 'mpegaudio_tables.h'), |
| os.path.join('libavcodec', 'pcm_tables.h'), |
| os.path.join('libavcodec', 'sinewin_tables.h'), |
| os.path.join('libavcodec', 'sinewin_fixed_tables.h'), |
| ] |
| |
| |
| # Regex to find lines matching #include "some_dir\some_file.h". |
| INCLUDE_REGEX = re.compile('#\s*include\s+"([^"]+)"') |
| |
| # Regex to find whacky includes that we might be overlooking (e.g. using macros |
| # or defines). |
| EXOTIC_INCLUDE_REGEX = re.compile('#\s*include\s+[^"<\s].+') |
| |
| |
| def GetIncludedSources(file_path, source_dir, include_set): |
| """Recurse over include tree, accumulating absolute paths to all included |
| files (including the seed file) in include_set. |
| |
| Pass in the set returned from previous calls to avoid re-walking parts of the |
| tree. Given file_path may be relative (to options.src_dir) or absolute. |
| |
| NOTE: This algorithm is greedy. It does not know which includes may be |
| excluded due to compile-time defines, so it considers any mentioned include. |
| |
| NOTE: This algorithm makes hard assumptions about the include search paths. |
| Paths are checked in the order: |
| 1. Directory of the file containing the #include directive |
| 2. Directory specified by source_dir |
| |
| NOTE: Files listed in IGNORED_INCLUDE_FILES will be ignored if not found. See |
| reasons at definition for IGNORED_INCLUDE_FILES. |
| """ |
| # Use options.source_dir to correctly resolve relative file path. Use only |
| # absolute paths in the set to avoid same-name-errors. |
| if not os.path.isabs(file_path): |
| file_path = os.path.abspath(os.path.join(source_dir, file_path)) |
| |
| current_dir = os.path.dirname(file_path) |
| |
| # Already processed this file, bail out. |
| if file_path in include_set: |
| return include_set |
| |
| include_set.add(file_path) |
| |
| for line in open(file_path): |
| include_match = INCLUDE_REGEX.search(line) |
| |
| if not include_match: |
| if EXOTIC_INCLUDE_REGEX.search(line): |
| print 'WARNING: Investigate whacky include line:', line |
| continue |
| |
| include_file_path = include_match.group(1) |
| |
| # These may or may not be where the file lives. Just storing temps here |
| # and we'll checking their validity below. |
| include_path_in_current_dir = os.path.join(current_dir, include_file_path) |
| include_path_in_source_dir = os.path.join(source_dir, include_file_path) |
| resolved_include_path = '' |
| |
| # Check if file is in current directory. |
| if os.path.isfile(include_path_in_current_dir): |
| resolved_include_path = include_path_in_current_dir |
| # Else, check source_dir (should be FFmpeg root). |
| elif os.path.isfile(include_path_in_source_dir): |
| resolved_include_path = include_path_in_source_dir |
| # Else, we couldn't find it :(. |
| elif include_file_path in IGNORED_INCLUDE_FILES: |
| continue |
| else: |
| exit('Failed to find file ' + include_file_path) |
| |
| # At this point we've found the file. Check if its in our ignore list which |
| # means that the list should be updated to no longer mention this file. |
| if include_file_path in IGNORED_INCLUDE_FILES: |
| print('Found %s in IGNORED_INCLUDE_FILES. Consider updating the list ' |
| 'to remove this file.' % str(include_file_path)) |
| |
| GetIncludedSources(resolved_include_path, source_dir, include_set) |
| |
| |
| def FixBasenameCollision(old_path, new_path, content): |
| with open(new_path, "w") as new_file: |
| new_file.write(content) |
| |
| |
| def UpdateCredits(sources_to_check, source_dir): |
| print 'Updating ffmpeg credits...' |
| updater = credits_updater.CreditsUpdater(source_dir) |
| for source_name in sources_to_check: |
| updater.ProcessFile(source_name) |
| updater.PrintStats() |
| updater.WriteCredits() |
| |
| |
| def main(): |
| options, _ = ParseOptions() |
| |
| # Generate map of FFmpeg source files. |
| source_dir = options.source_dir |
| source_files = GetSourceFiles(source_dir) |
| object_to_sources = GetObjectToSourceMapping(source_files) |
| |
| sets = [] |
| |
| for arch in SUPPORT_MATRIX[Attr.ARCHITECTURE]: |
| for profile in SUPPORT_MATRIX[Attr.PROFILE]: |
| # Assume build directory is of the form build.$arch/$profile. |
| name = ''.join(['build.', arch]) |
| build_dir = os.path.join(options.build_dir, name, profile) |
| if not os.path.exists(build_dir): |
| continue |
| print 'Processing build directory: %s' % name |
| |
| object_files = GetObjectFiles(build_dir) |
| |
| # Generate the set of source files to build said profile. |
| s = GetSourceFileSet(object_to_sources, object_files) |
| sets.append(SourceSet(s, set([SourceListCondition(arch, profile)]))) |
| |
| sets = CreatePairwiseDisjointSets(sets) |
| |
| for source_set in sets: |
| ReduceConditionalLogic(source_set) |
| |
| if not sets: |
| exit('ERROR: failed to find any source sets. ' + |
| 'Are build_dir (%s) and/or source_dir (%s) options correct?' % |
| (options.build_dir, options.source_dir)) |
| |
| # Build up set of all sources and includes. |
| sources_to_check = set() |
| for source_set in sets: |
| for source in source_set.sources: |
| GetIncludedSources(source, source_dir, sources_to_check) |
| |
| UpdateCredits(sources_to_check, source_dir) |
| |
| gn_file_name = os.path.join(options.source_dir, 'ffmpeg_generated.gni') |
| print 'Writing:', gn_file_name |
| with open(gn_file_name, 'w') as fd: |
| WriteGn(fd, sets) |
| |
| |
| if __name__ == '__main__': |
| main() |