1#!/usr/bin/env python
2#
3# Copyright 2014 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""
7Utilities for the modular DevTools build.
8"""
9
10import collections
11from os import path
12import os
13
14try:
15    import simplejson as json
16except ImportError:
17    import json
18
19
20def read_file(filename):
21    with open(path.normpath(filename), 'rt') as input:
22        return input.read()
23
24
25def write_file(filename, content):
26    if path.exists(filename):
27        os.remove(filename)
28    directory = path.dirname(filename)
29    if not path.exists(directory):
30        os.makedirs(directory)
31    with open(filename, 'wt') as output:
32        output.write(content)
33
34
35def bail_error(message):
36    raise Exception(message)
37
38
39def load_and_parse_json(filename):
40    try:
41        return json.loads(read_file(filename))
42    except:
43        print 'ERROR: Failed to parse %s' % filename
44        raise
45
46
47def concatenate_scripts(file_names, module_dir, output_dir, output):
48    for file_name in file_names:
49        output.write('/* %s */\n' % file_name)
50        file_path = path.join(module_dir, file_name)
51        if not path.isfile(file_path):
52            file_path = path.join(output_dir, path.basename(module_dir), file_name)
53        output.write(read_file(file_path))
54        output.write(';')
55
56
57class Descriptors:
58
59    def __init__(self, application_name, application_dir, application_descriptor, module_descriptors, extends, has_html, worker):
60        self.application_name = application_name
61        self.application_dir = application_dir
62        self.application = application_descriptor
63        self._cached_sorted_modules = None
64        self.modules = module_descriptors
65        self.extends = extends
66        self.has_html = has_html
67        self.worker = worker
68
69    def application_json(self):
70        result = dict()
71        result['modules'] = self.application.values()
72        result['has_html'] = self.has_html
73        return json.dumps(result)
74
75    def all_compiled_files(self):
76        files = collections.OrderedDict()
77        for name in self.sorted_modules():
78            module = self.modules[name]
79            skipped_files = set(module.get('skip_compilation', []))
80            for script in module.get('scripts', []) + module.get('modules', []):
81                if script not in skipped_files:
82                    files[path.normpath(path.join(self.application_dir, name, script))] = True
83        return files.keys()
84
85    def all_skipped_compilation_files(self):
86        files = collections.OrderedDict()
87        for name in self.sorted_modules():
88            module = self.modules[name]
89            skipped_files = set(module.get('skip_compilation', []))
90            for script in skipped_files:
91                files[path.join(name, script)] = True
92        return files.keys()
93
94    def module_compiled_files(self, name):
95        files = []
96        module = self.modules.get(name)
97        skipped_files = set(module.get('skip_compilation', []))
98        for script in module.get('scripts', []):
99            if script not in skipped_files:
100                files.append(script)
101        return files
102
103    def module_resources(self, name):
104        return [name + '/' + resource for resource in self.modules[name].get('resources', [])]
105
106    def sorted_modules(self):
107        if self._cached_sorted_modules:
108            return self._cached_sorted_modules
109
110        result = []
111        unvisited_modules = set(self.modules)
112        temp_modules = set()
113
114        def visit(parent, name):
115            if name not in unvisited_modules:
116                return None
117            if name not in self.modules:
118                return (parent, name)
119            if name in temp_modules:
120                bail_error('Dependency cycle found at module "%s"' % name)
121            temp_modules.add(name)
122            deps = self.modules[name].get('dependencies')
123            if deps:
124                for dep_name in deps:
125                    bad_dep = visit(name, dep_name)
126                    if bad_dep:
127                        return bad_dep
128            unvisited_modules.remove(name)
129            temp_modules.remove(name)
130            result.append(name)
131            return None
132
133        while len(unvisited_modules):
134            for next in unvisited_modules:
135                break
136            failure = visit(None, next)
137            if failure:
138                # failure[0] can never be None
139                bail_error('Unknown module "%s" encountered in dependencies of "%s"' % (failure[1], failure[0]))
140
141        self._cached_sorted_modules = result
142        return result
143
144    def sorted_dependencies_closure(self, module_name):
145        visited = set()
146
147        def sorted_deps_for_module(name):
148            result = []
149            desc = self.modules[name]
150            deps = desc.get('dependencies', [])
151            for dep in deps:
152                result += sorted_deps_for_module(dep)
153            if name not in visited:
154                result.append(name)
155                visited.add(name)
156            return result
157
158        return sorted_deps_for_module(module_name)
159
160
161class DescriptorLoader:
162
163    def __init__(self, application_dir):
164        self.application_dir = application_dir
165
166    def load_application(self, application_descriptor_name):
167        all_module_descriptors = {}
168        result = self._load_application(application_descriptor_name, all_module_descriptors)
169        return result
170
171    def load_applications(self, application_descriptor_names):
172        all_module_descriptors = {}
173        all_application_descriptors = {}
174        for application_descriptor_name in application_descriptor_names:
175            descriptors = {}
176            result = self._load_application(application_descriptor_name, descriptors)
177            for name in descriptors:
178                all_module_descriptors[name] = descriptors[name]
179            for name in result.application:
180                all_application_descriptors[name] = result.application[name]
181        return Descriptors('all', self.application_dir, all_application_descriptors, all_module_descriptors, None, False, False)
182
183    def _load_application(self, application_descriptor_name, all_module_descriptors):
184        module_descriptors = {}
185        application_descriptor_filename = path.join(self.application_dir, application_descriptor_name + '.json')
186        descriptor_json = load_and_parse_json(application_descriptor_filename)
187        application_descriptor = {desc['name']: desc for desc in descriptor_json['modules']}
188        extends = descriptor_json['extends'] if 'extends' in descriptor_json else None
189        if extends:
190            extends = self._load_application(extends, all_module_descriptors)
191        has_html = True if 'has_html' in descriptor_json and descriptor_json['has_html'] else False
192        worker = True if 'worker' in descriptor_json and descriptor_json['worker'] else False
193
194        for (module_name, module) in application_descriptor.items():
195            if all_module_descriptors.get(module_name):
196                bail_error('Duplicate definition of module "%s" in %s' % (module_name, application_descriptor_filename))
197            module_descriptors[module_name] = self._read_module_descriptor(module_name, application_descriptor_filename)
198            all_module_descriptors[module_name] = module_descriptors[module_name]
199
200        for module in module_descriptors.values():
201            for dep in module.get('dependencies', []):
202                if dep not in all_module_descriptors:
203                    bail_error('Module "%s" (dependency of "%s") not listed in application descriptor %s' %
204                               (dep, module['name'], application_descriptor_filename))
205
206        return Descriptors(application_descriptor_name, self.application_dir, application_descriptor, module_descriptors, extends,
207                           has_html, worker)
208
209    def _read_module_descriptor(self, module_name, application_descriptor_filename):
210        json_filename = path.join(self.application_dir, module_name, 'module.json')
211        if not path.exists(json_filename):
212            bail_error('Module descriptor %s referenced in %s is missing' % (json_filename, application_descriptor_filename))
213        module_json = load_and_parse_json(json_filename)
214        module_json['name'] = module_name
215        return module_json
216