[GTK][CMake] Add a 'distcheck' target
[WebKit-https.git] / Tools / gtk / make-dist.py
1 #!/usr/bin/env python
2 # Copyright (C) 2014 Igalia S.L.
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2 of the License, or (at your option) any later version.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17
18 from __future__ import print_function
19 from contextlib import closing
20
21 import argparse
22 import errno
23 import multiprocessing
24 import os
25 import re
26 import shutil
27 import subprocess
28 import tarfile
29
30
31 def enum(**enums):
32     return type('Enum', (), enums)
33
34
35 class Rule(object):
36     Result = enum(INCLUDE=1, EXCLUDE=2, NO_MATCH=3)
37
38     def __init__(self, type, pattern):
39         self.type = type
40         self.original_pattern = pattern
41         self.pattern = re.compile(pattern)
42
43     def test(self, file):
44         if not(self.pattern.search(file)):
45             return Rule.Result.NO_MATCH
46         return self.type
47
48
49 class Ruleset(object):
50     _global_rules = None
51
52     def __init__(self):
53         # By default, accept all files.
54         self.rules = [Rule(Rule.Result.INCLUDE, '.*')]
55
56     @classmethod
57     def global_rules(cls):
58         if not cls._global_rules:
59             cls._global_rules = Ruleset()
60         return cls._global_rules
61
62     @classmethod
63     def add_global_rule(cls, rule):
64         cls.global_rules().add_rule(rule)
65
66     def add_rule(self, rule):
67         self.rules.append(rule)
68
69     def passes(self, file):
70         allowed = False
71         for rule in self.rules:
72             result = rule.test(file)
73             if result == Rule.Result.NO_MATCH:
74                 continue
75             allowed = Rule.Result.INCLUDE == result
76         return allowed
77
78
79 class File(object):
80     def __init__(self, source_root, tarball_root):
81         self.source_root = source_root
82         self.tarball_root = tarball_root
83
84     def get_files(self):
85         yield (self.source_root, self.tarball_root)
86
87
88 class Directory(object):
89     def __init__(self, source_root, tarball_root):
90         self.source_root = source_root
91         self.tarball_root = tarball_root
92         self.rules = Ruleset()
93
94     def add_rule(self, rule):
95         self.rules.add_rule(rule)
96
97     def get_tarball_path(self, filename):
98         return filename.replace(self.source_root, self.tarball_root, 1)
99
100     def get_files(self):
101         for root, dirs, files in os.walk(self.source_root):
102
103             def passes_all_rules(entry):
104                 return Ruleset.global_rules().passes(entry) and self.rules.passes(entry)
105
106             to_keep = filter(passes_all_rules, dirs)
107             del dirs[:]
108             dirs.extend(to_keep)
109
110             for file in files:
111                 file = os.path.join(root, file)
112                 if not passes_all_rules(file):
113                     continue
114                 yield (file, self.get_tarball_path(file))
115
116
117 class Manifest(object):
118     def __init__(self, manifest_filename, source_root, build_root, tarball_root='/'):
119         self.current_directory = None
120         self.directories = []
121         self.tarball_root = tarball_root
122         self.source_root = source_root
123         self.build_root = build_root
124
125         # Normalize the tarball root so that it starts and ends with a slash.
126         if not self.tarball_root.endswith('/'):
127             self.tarball_root = self.tarball_root + '/'
128         if not self.tarball_root.startswith('/'):
129             self.tarball_root = '/' + self.tarball_root
130
131         with open(manifest_filename, 'r') as file:
132             for line in file.readlines():
133                 self.process_line(line)
134
135     def add_rule(self, rule):
136         if self.current_directory is not None:
137             self.current_directory.add_rule(rule)
138         else:
139             Ruleset.add_global_rule(rule)
140
141     def add_directory(self, directory):
142         self.current_directory = directory
143         self.directories.append(directory)
144
145     def resolve_variables(self, string, strip=False):
146         if strip:
147             return string.replace('$source', '').replace('$build', '')
148
149         string = string.replace('$source', self.source_root)
150         if self.build_root:
151             string = string.replace('$build', self.build_root)
152         elif string.find('$build') != -1:
153             raise Exception('Manifest has $build but build root not given.')
154         return string
155
156     def get_full_source_path(self, source_path):
157         full_source_path = self.resolve_variables(source_path)
158         if not os.path.exists(full_source_path):
159             full_source_path = os.path.join(self.source_root, source_path)
160         if not os.path.exists(full_source_path):
161             raise Exception('Could not find directory %s' % full_source_path)
162         return full_source_path
163
164     def get_full_tarball_path(self, path):
165         path = self.resolve_variables(path, strip=True)
166         return self.tarball_root + path
167
168     def get_source_and_tarball_paths_from_parts(self, parts):
169         full_source_path = self.get_full_source_path(parts[1])
170         if len(parts) > 2:
171             full_tarball_path = self.get_full_tarball_path(parts[2])
172         else:
173             full_tarball_path = self.get_full_tarball_path(parts[1])
174         return (full_source_path, full_tarball_path)
175
176     def process_line(self, line):
177         parts = line.split()
178         if not parts:
179             return
180         if parts[0].startswith("#"):
181             return
182
183         if parts[0] == "directory" and len(parts) > 1:
184             self.add_directory(Directory(*self.get_source_and_tarball_paths_from_parts(parts)))
185         elif parts[0] == "file" and len(parts) > 1:
186             self.add_directory(File(*self.get_source_and_tarball_paths_from_parts(parts)))
187         elif parts[0] == "exclude" and len(parts) > 1:
188             self.add_rule(Rule(Rule.Result.EXCLUDE, self.resolve_variables(parts[1])))
189         elif parts[0] == "include" and len(parts) > 1:
190             self.add_rule(Rule(Rule.Result.INCLUDE, self.resolve_variables(parts[1])))
191
192     def get_files(self):
193         for directory in self.directories:
194             for file_tuple in directory.get_files():
195                 yield file_tuple
196
197     def create_tarfile(self, output):
198         count = 0
199         for file_tuple in self.get_files():
200             count = count + 1
201
202         with closing(tarfile.open(output, 'w')) as tarball:
203             for i, (file_path, tarball_path) in enumerate(self.get_files(), start=1):
204                 print('Tarring file {0} of {1}'.format(i, count).ljust(40), end='\r')
205                 tarball.add(file_path, tarball_path)
206         print("Wrote {0}".format(output).ljust(40))
207
208
209 class Distcheck(object):
210     BUILD_DIRECTORY_NAME = "_build"
211     INSTALL_DIRECTORY_NAME = "_install"
212
213     def __init__(self, source_root, build_root):
214         self.source_root = source_root
215         self.build_root = build_root
216
217     def extract_tarball(self, tarball_path):
218         with closing(tarfile.open(tarball_path, 'r')) as tarball:
219             tarball.extractall(self.build_root)
220
221     def configure(self, build_dir, install_dir):
222         def create_dir(directory, directory_type):
223             try:
224                 os.mkdir(directory)
225             except OSError, e:
226                 if e.errno != errno.EEXIST or not os.path.isdir(directory):
227                     raise Exception("Could not create %s dir at %s: %s" % (directory_type, directory, str(e)))
228
229         create_dir(build_dir, "build")
230         create_dir(install_dir, "install")
231
232         command = ['cmake', '-DPORT=GTK', '-DCMAKE_INSTALL_PREFIX=%s' % install_dir, '-DCMAKE_BUILD_TYPE=Release', self.source_root]
233         subprocess.check_call(command, cwd=build_dir)
234
235     def build(self, build_dir):
236         command = ['make']
237         make_args = os.getenv('MAKE_ARGS')
238         if make_args:
239             command.extend(make_args.split(' '))
240         else:
241             command.append('-j%d' % multiprocessing.cpu_count())
242         subprocess.check_call(command, cwd=build_dir)
243
244     def install(self, build_dir):
245         subprocess.check_call(['make', 'install'], cwd=build_dir)
246
247     def clean(self, dist_dir):
248         shutil.rmtree(dist_dir)
249
250     def check(self, tarball):
251         tarball_name, ext = os.path.splitext(os.path.basename(tarball))
252         dist_dir = os.path.join(self.build_root, tarball_name)
253         build_dir = os.path.join(dist_dir, self.BUILD_DIRECTORY_NAME)
254         install_dir = os.path.join(dist_dir, self.INSTALL_DIRECTORY_NAME)
255
256         self.extract_tarball(tarball)
257         self.configure(build_dir, install_dir)
258         self.build(build_dir)
259         self.install(build_dir)
260         self.clean(dist_dir)
261
262 if __name__ == "__main__":
263     class FilePathAction(argparse.Action):
264         def __call__(self, parser, namespace, values, option_string=None):
265             setattr(namespace, self.dest, os.path.abspath(values))
266
267     parser = argparse.ArgumentParser(description='Build a distribution bundle.')
268     parser.add_argument('-c', '--check', action='store_true',
269                         help='Check the tarball')
270     parser.add_argument('-s', '--source-dir', type=str, action=FilePathAction, default=os.getcwd(),
271                         help='The top-level directory of the source distribution. ' + \
272                               'Directory for relative paths. Defaults to current directory.')
273     parser.add_argument('--tarball-root', type=str, default='/',
274                         help='The top-level path of the tarball. By default files are added to the root of the tarball.')
275     parser.add_argument('-b', '--build-dir', type=str, action=FilePathAction, default=None,
276                         help='The top-level path of directory of the build root. ' + \
277                               'By default there is no build root.')
278     parser.add_argument('-o', type=str, action=FilePathAction, default='out.tar', dest="output_filename",
279                         help='The tarfile to produce. By default this is "out.tar"')
280     parser.add_argument('manifest_filename', metavar="manifest", type=str, action=FilePathAction, help='The path to the manifest file.')
281
282     arguments = parser.parse_args()
283
284     # Paths in the manifest are relative to the source directory, and this script assumes that
285     # current working directory is the source directory, so change the current working directory
286     # to be the source directory.
287     os.chdir(arguments.source_dir)
288
289     manifest = Manifest(arguments.manifest_filename, arguments.source_dir, arguments.build_dir, arguments.tarball_root)
290     manifest.create_tarfile(arguments.output_filename)
291
292     if arguments.check:
293         Distcheck(arguments.source_dir, arguments.build_dir).check(arguments.output_filename)