| #!/usr/bin/env python3 |
| # Copyright (C) 2014 Igalia S.L. |
| # |
| # This library is free software; you can redistribute it and/or |
| # modify it under the terms of the GNU Lesser General Public |
| # License as published by the Free Software Foundation; either |
| # version 2 of the License, or (at your option) any later version. |
| # |
| # This library is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| # Lesser General Public License for more details. |
| # |
| # You should have received a copy of the GNU Lesser General Public |
| # License along with this library; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| |
| from __future__ import print_function |
| from contextlib import closing |
| |
| import argparse |
| import errno |
| import multiprocessing |
| import os |
| import re |
| import shutil |
| import subprocess |
| import tarfile |
| |
| |
| def enum(**enums): |
| return type('Enum', (), enums) |
| |
| |
| class Rule(object): |
| Result = enum(INCLUDE=1, EXCLUDE=2, NO_MATCH=3) |
| |
| def __init__(self, type, pattern): |
| self.type = type |
| self.original_pattern = pattern |
| self.pattern = re.compile(pattern) |
| |
| def test(self, file): |
| if not(self.pattern.search(file)): |
| return Rule.Result.NO_MATCH |
| return self.type |
| |
| |
| class Ruleset(object): |
| _global_rules = None |
| |
| def __init__(self): |
| # By default, accept all files. |
| self.rules = [Rule(Rule.Result.INCLUDE, '.*')] |
| |
| @classmethod |
| def global_rules(cls): |
| if not cls._global_rules: |
| cls._global_rules = Ruleset() |
| return cls._global_rules |
| |
| @classmethod |
| def add_global_rule(cls, rule): |
| cls.global_rules().add_rule(rule) |
| |
| def add_rule(self, rule): |
| self.rules.append(rule) |
| |
| def passes(self, file): |
| allowed = False |
| for rule in self.rules: |
| result = rule.test(file) |
| if result == Rule.Result.NO_MATCH: |
| continue |
| allowed = Rule.Result.INCLUDE == result |
| return allowed |
| |
| |
| class File(object): |
| def __init__(self, source_root, tarball_root): |
| self.source_root = source_root |
| self.tarball_root = tarball_root |
| |
| def should_skip_file(self, path): |
| # Do not skip files explicitly added from the manifest. |
| return False |
| |
| def get_files(self): |
| yield (self.source_root, self.tarball_root) |
| |
| |
| class Directory(object): |
| def __init__(self, source_root, tarball_root): |
| self.source_root = source_root |
| self.tarball_root = tarball_root |
| self.rules = Ruleset() |
| |
| self.files_in_version_control = self.list_files_in_version_control() |
| |
| def add_rule(self, rule): |
| self.rules.add_rule(rule) |
| |
| def get_tarball_path(self, filename): |
| return filename.replace(self.source_root, self.tarball_root, 1) |
| |
| def list_files_in_version_control(self): |
| # FIXME: Only git is supported for now. |
| p = subprocess.Popen(['git', 'ls-tree', '-r', '--name-only', 'HEAD', self.source_root], stdout=subprocess.PIPE, text="ascii") |
| out = p.communicate()[0] |
| if not out: |
| return [] |
| return frozenset(out.splitlines()) |
| |
| def should_skip_file(self, path): |
| return path not in self.files_in_version_control |
| |
| def get_files(self): |
| for root, dirs, files in os.walk(self.source_root): |
| |
| def passes_all_rules(entry): |
| return Ruleset.global_rules().passes(entry) and self.rules.passes(entry) |
| |
| to_keep = list(filter(passes_all_rules, dirs)) |
| del dirs[:] |
| dirs.extend(to_keep) |
| |
| for file in files: |
| file = os.path.join(root, file) |
| if not passes_all_rules(file): |
| continue |
| yield (file, self.get_tarball_path(file)) |
| |
| |
| class Manifest(object): |
| def __init__(self, manifest_filename, source_root, build_root, tarball_root='/'): |
| self.current_directory = None |
| self.directories = [] |
| self.tarball_root = tarball_root |
| self.source_root = source_root |
| self.build_root = build_root |
| |
| # Normalize the tarball root so that it starts and ends with a slash. |
| if not self.tarball_root.endswith('/'): |
| self.tarball_root = self.tarball_root + '/' |
| if not self.tarball_root.startswith('/'): |
| self.tarball_root = '/' + self.tarball_root |
| |
| with open(manifest_filename, 'r') as file: |
| for line in file.readlines(): |
| self.process_line(line) |
| |
| def add_rule(self, rule): |
| if self.current_directory is not None: |
| self.current_directory.add_rule(rule) |
| else: |
| Ruleset.add_global_rule(rule) |
| |
| def add_directory(self, directory): |
| self.current_directory = directory |
| self.directories.append(directory) |
| |
| def get_full_source_path(self, source_path): |
| if not os.path.exists(source_path): |
| source_path = os.path.join(self.source_root, source_path) |
| if not os.path.exists(source_path): |
| raise Exception('Could not find directory %s' % source_path) |
| return source_path |
| |
| def get_full_tarball_path(self, path): |
| return self.tarball_root + path |
| |
| def get_source_and_tarball_paths_from_parts(self, parts): |
| full_source_path = self.get_full_source_path(parts[1]) |
| if len(parts) > 2: |
| full_tarball_path = self.get_full_tarball_path(parts[2]) |
| else: |
| full_tarball_path = self.get_full_tarball_path(parts[1]) |
| return (full_source_path, full_tarball_path) |
| |
| def process_line(self, line): |
| parts = line.split() |
| if not parts: |
| return |
| if parts[0].startswith("#"): |
| return |
| |
| if parts[0] == "directory" and len(parts) > 1: |
| self.add_directory(Directory(*self.get_source_and_tarball_paths_from_parts(parts))) |
| elif parts[0] == "file" and len(parts) > 1: |
| self.add_directory(File(*self.get_source_and_tarball_paths_from_parts(parts))) |
| elif parts[0] == "exclude" and len(parts) > 1: |
| self.add_rule(Rule(Rule.Result.EXCLUDE, parts[1])) |
| elif parts[0] == "include" and len(parts) > 1: |
| self.add_rule(Rule(Rule.Result.INCLUDE, parts[1])) |
| else: |
| raise Exception('Line does not begin with a correct rule:\n\t' + line) |
| |
| def should_skip_file(self, directory, filename): |
| # Only allow files that are not in version control when they are explicitly included in the manifest from the build dir. |
| if filename.startswith(self.build_root): |
| return False |
| |
| return directory.should_skip_file(filename) |
| |
| def get_files(self): |
| for directory in self.directories: |
| for file_tuple in directory.get_files(): |
| if self.should_skip_file(directory, file_tuple[0]): |
| continue |
| yield file_tuple |
| |
| def create_tarfile(self, output): |
| count = 0 |
| for file_tuple in self.get_files(): |
| count = count + 1 |
| |
| with closing(tarfile.open(output, 'w')) as tarball: |
| for i, (file_path, tarball_path) in enumerate(self.get_files(), start=1): |
| print('Tarring file {0} of {1}'.format(i, count).ljust(40), end='\r') |
| tarball.add(file_path, tarball_path) |
| print("Wrote {0}".format(output).ljust(40)) |
| |
| |
| class Distcheck(object): |
| BUILD_DIRECTORY_NAME = "_build" |
| INSTALL_DIRECTORY_NAME = "_install" |
| |
| def __init__(self, source_root, build_root): |
| self.source_root = source_root |
| self.build_root = build_root |
| |
| def extract_tarball(self, tarball_path): |
| with closing(tarfile.open(tarball_path, 'r')) as tarball: |
| tarball.extractall(self.build_root) |
| |
| def configure(self, dist_dir, build_dir, install_dir, port): |
| def create_dir(directory, directory_type): |
| try: |
| os.mkdir(directory) |
| except OSError as e: |
| if e.errno != errno.EEXIST or not os.path.isdir(directory): |
| raise Exception("Could not create %s dir at %s: %s" % (directory_type, directory, str(e))) |
| |
| create_dir(build_dir, "build") |
| create_dir(install_dir, "install") |
| |
| command = ['cmake', '-DPORT=%s' % port, '-DCMAKE_INSTALL_PREFIX=%s' % install_dir, '-DCMAKE_BUILD_TYPE=Release', '-DENABLE_GTKDOC=ON', '-DENABLE_MINIBROWSER=ON', dist_dir] |
| subprocess.check_call(command, cwd=build_dir) |
| |
| def build(self, build_dir): |
| command = ['make'] |
| make_args = os.getenv('MAKE_ARGS') |
| if make_args: |
| command.extend(make_args.split(' ')) |
| else: |
| command.append('-j%d' % multiprocessing.cpu_count()) |
| subprocess.check_call(command, cwd=build_dir) |
| |
| def check_symbols(self, build_dir): |
| check_bss = os.path.join(self.source_root, 'Tools', 'Scripts', 'check-for-global-bss-symbols-in-webkitgtk-libs') |
| libjsc = os.path.join(build_dir, 'lib', 'libjavascriptcoregtk-4.1.so') |
| libwk = os.path.join(build_dir, 'lib', 'libwebkit2gtk-4.1.so') |
| subprocess.check_call([check_bss, libjsc, libwk]) |
| |
| check_version_script = os.path.join(self.source_root, 'Tools', 'Scripts', 'check-for-invalid-symbols-in-version-script') |
| version_script = os.path.join(os.path.dirname(build_dir), 'Source', 'WebKit', 'webkitglib-symbols.map') |
| subprocess.check_call([check_version_script, version_script, libwk]) |
| |
| def install(self, build_dir): |
| subprocess.check_call(['make', 'install'], cwd=build_dir) |
| |
| def clean(self, dist_dir): |
| shutil.rmtree(dist_dir) |
| |
| def check(self, tarball, port): |
| tarball_name, ext = os.path.splitext(os.path.basename(tarball)) |
| dist_dir = os.path.join(self.build_root, tarball_name) |
| build_dir = os.path.join(dist_dir, self.BUILD_DIRECTORY_NAME) |
| install_dir = os.path.join(dist_dir, self.INSTALL_DIRECTORY_NAME) |
| |
| self.extract_tarball(tarball) |
| self.configure(dist_dir, build_dir, install_dir, port) |
| self.build(build_dir) |
| if port == 'GTK': |
| self.check_symbols(build_dir) |
| self.install(build_dir) |
| self.clean(dist_dir) |
| |
| if __name__ == "__main__": |
| class FilePathAction(argparse.Action): |
| def __call__(self, parser, namespace, values, option_string=None): |
| setattr(namespace, self.dest, os.path.abspath(values)) |
| |
| def ensure_version_if_possible(arguments): |
| if arguments.version is not None: |
| return |
| |
| pkgconfig_file = os.path.join(arguments.build_dir, "Source/WebKit/webkit2gtk-4.1.pc") |
| if os.path.isfile(pkgconfig_file): |
| p = subprocess.Popen(['pkg-config', '--modversion', pkgconfig_file], stdout=subprocess.PIPE, text="ascii") |
| version = p.communicate()[0] |
| if version: |
| arguments.version = version.rstrip('\n') |
| |
| |
| def get_tarball_root_and_output_filename_from_arguments(arguments): |
| tarball_root = arguments.tarball_name |
| if arguments.version is not None: |
| tarball_root += '-' + arguments.version |
| |
| output_filename = os.path.join(arguments.build_dir, tarball_root + ".tar") |
| return tarball_root, output_filename |
| |
| parser = argparse.ArgumentParser(description='Build a distribution bundle.') |
| parser.add_argument('-c', '--check', action='store_true', |
| help='Check the tarball') |
| parser.add_argument('-s', '--source-dir', type=str, action=FilePathAction, default=os.getcwd(), |
| help='The top-level directory of the source distribution. ' + \ |
| 'Directory for relative paths. Defaults to current directory.') |
| parser.add_argument('--version', type=str, default=None, |
| help='The version of the tarball to generate') |
| parser.add_argument('-b', '--build-dir', type=str, action=FilePathAction, default=os.getcwd(), |
| help='The top-level path of directory of the build root. ' + \ |
| 'By default is the current directory.') |
| parser.add_argument('-t', '--tarball-name', type=str, default='webkitgtk', |
| help='Base name of tarball. Defaults to "webkitgtk".') |
| parser.add_argument('-p', '--port', type=str, default='GTK', |
| help='Port to be built in tarball check. Defaults to "GTK".') |
| parser.add_argument('manifest_filename', metavar="manifest", type=str, action=FilePathAction, help='The path to the manifest file.') |
| |
| arguments = parser.parse_args() |
| |
| # Paths in the manifest are relative to the source directory, and this script assumes that |
| # current working directory is the source directory, so change the current working directory |
| # to be the source directory. |
| os.chdir(arguments.source_dir) |
| |
| ensure_version_if_possible(arguments) |
| tarball_root, output_filename = get_tarball_root_and_output_filename_from_arguments(arguments) |
| |
| manifest = Manifest(arguments.manifest_filename, arguments.source_dir, arguments.build_dir, tarball_root) |
| manifest.create_tarfile(output_filename) |
| |
| if arguments.check: |
| Distcheck(arguments.source_dir, arguments.build_dir).check(output_filename, arguments.port) |