1# Copyright 2020 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import json
16import os
17import pathlib
18import pickle
19import re
20import sys
21import typing as T
22
23from ..backend.ninjabackend import TargetDependencyScannerInfo, ninja_quote
24from ..compilers.compilers import lang_suffixes
25
26CPP_IMPORT_RE = re.compile(r'\w*import ([a-zA-Z0-9]+);')
27CPP_EXPORT_RE = re.compile(r'\w*export module ([a-zA-Z0-9]+);')
28
29FORTRAN_INCLUDE_PAT = r"^\s*include\s*['\"](\w+\.\w+)['\"]"
30FORTRAN_MODULE_PAT = r"^\s*\bmodule\b\s+(\w+)\s*(?:!+.*)*$"
31FORTRAN_SUBMOD_PAT = r"^\s*\bsubmodule\b\s*\((\w+:?\w+)\)\s*(\w+)"
32FORTRAN_USE_PAT = r"^\s*use,?\s*(?:non_intrinsic)?\s*(?:::)?\s*(\w+)"
33
34FORTRAN_MODULE_RE = re.compile(FORTRAN_MODULE_PAT, re.IGNORECASE)
35FORTRAN_SUBMOD_RE = re.compile(FORTRAN_SUBMOD_PAT, re.IGNORECASE)
36FORTRAN_USE_RE = re.compile(FORTRAN_USE_PAT, re.IGNORECASE)
37
38class DependencyScanner:
39    def __init__(self, pickle_file: str, outfile: str, sources: T.List[str]):
40        with open(pickle_file, 'rb') as pf:
41            self.target_data = pickle.load(pf) # type: TargetDependencyScannerInfo
42        self.outfile = outfile
43        self.sources = sources
44        self.provided_by = {} # type: T.Dict[str, str]
45        self.exports = {} # type: T.Dict[str, str]
46        self.needs = {} # type: T.Dict[str, T.List[str]]
47        self.sources_with_exports = [] # type: T.List[str]
48
49    def scan_file(self, fname: str) -> None:
50        suffix = os.path.splitext(fname)[1][1:].lower()
51        if suffix in lang_suffixes['fortran']:
52            self.scan_fortran_file(fname)
53        elif suffix in lang_suffixes['cpp']:
54            self.scan_cpp_file(fname)
55        else:
56            sys.exit(f'Can not scan files with suffix .{suffix}.')
57
58    def scan_fortran_file(self, fname: str) -> None:
59        fpath = pathlib.Path(fname)
60        modules_in_this_file = set()
61        for line in fpath.read_text(encoding='utf-8').split('\n'):
62            import_match = FORTRAN_USE_RE.match(line)
63            export_match = FORTRAN_MODULE_RE.match(line)
64            submodule_export_match = FORTRAN_SUBMOD_RE.match(line)
65            if import_match:
66                needed = import_match.group(1).lower()
67                # In Fortran you have an using declaration also for the module
68                # you define in the same file. Prevent circular dependencies.
69                if needed not in modules_in_this_file:
70                    if fname in self.needs:
71                        self.needs[fname].append(needed)
72                    else:
73                        self.needs[fname] = [needed]
74            if export_match:
75                exported_module = export_match.group(1).lower()
76                assert exported_module not in modules_in_this_file
77                modules_in_this_file.add(exported_module)
78                if exported_module in self.provided_by:
79                    raise RuntimeError(f'Multiple files provide module {exported_module}.')
80                self.sources_with_exports.append(fname)
81                self.provided_by[exported_module] = fname
82                self.exports[fname] = exported_module
83            if submodule_export_match:
84                # Store submodule "Foo" "Bar" as "foo:bar".
85                # A submodule declaration can be both an import and an export declaration:
86                #
87                # submodule (a1:a2) a3
88                #  - requires a1@a2.smod
89                #  - produces a1@a3.smod
90                parent_module_name_full = submodule_export_match.group(1).lower()
91                parent_module_name = parent_module_name_full.split(':')[0]
92                submodule_name = submodule_export_match.group(2).lower()
93                concat_name = f'{parent_module_name}:{submodule_name}'
94                self.sources_with_exports.append(fname)
95                self.provided_by[concat_name] = fname
96                self.exports[fname] = concat_name
97                # Fortran requires that the immediate parent module must be built
98                # before the current one. Thus:
99                #
100                # submodule (parent) parent   <- requires parent.mod (really parent.smod, but they are created at the same time)
101                # submodule (a1:a2) a3        <- requires a1@a2.smod
102                #
103                # a3 does not depend on the a1 parent module directly, only transitively.
104                if fname in self.needs:
105                    self.needs[fname].append(parent_module_name_full)
106                else:
107                    self.needs[fname] = [parent_module_name_full]
108
109    def scan_cpp_file(self, fname: str) -> None:
110        fpath = pathlib.Path(fname)
111        for line in fpath.read_text(encoding='utf-8').split('\n'):
112            import_match = CPP_IMPORT_RE.match(line)
113            export_match = CPP_EXPORT_RE.match(line)
114            if import_match:
115                needed = import_match.group(1)
116                if fname in self.needs:
117                    self.needs[fname].append(needed)
118                else:
119                    self.needs[fname] = [needed]
120            if export_match:
121                exported_module = export_match.group(1)
122                if exported_module in self.provided_by:
123                    raise RuntimeError(f'Multiple files provide module {exported_module}.')
124                self.sources_with_exports.append(fname)
125                self.provided_by[exported_module] = fname
126                self.exports[fname] = exported_module
127
128    def objname_for(self, src: str) -> str:
129        objname = self.target_data.source2object[src]
130        assert isinstance(objname, str)
131        return objname
132
133    def module_name_for(self, src: str) -> str:
134        suffix = os.path.splitext(src)[1][1:].lower()
135        if suffix in lang_suffixes['fortran']:
136            exported = self.exports[src]
137            # Module foo:bar goes to a file name foo@bar.smod
138            # Module Foo goes to a file name foo.mod
139            namebase = exported.replace(':', '@')
140            if ':' in exported:
141                extension = 'smod'
142            else:
143                extension = 'mod'
144            return os.path.join(self.target_data.private_dir, f'{namebase}.{extension}')
145        elif suffix in lang_suffixes['cpp']:
146            return '{}.ifc'.format(self.exports[src])
147        else:
148            raise RuntimeError('Unreachable code.')
149
150    def scan(self) -> int:
151        for s in self.sources:
152            self.scan_file(s)
153        with open(self.outfile, 'w', encoding='utf-8') as ofile:
154            ofile.write('ninja_dyndep_version = 1\n')
155            for src in self.sources:
156                objfilename = self.objname_for(src)
157                mods_and_submods_needed = []
158                module_files_generated = []
159                module_files_needed = []
160                if src in self.sources_with_exports:
161                    module_files_generated.append(self.module_name_for(src))
162                if src in self.needs:
163                    for modname in self.needs[src]:
164                        if modname not in self.provided_by:
165                            # Nothing provides this module, we assume that it
166                            # comes from a dependency library somewhere and is
167                            # already built by the time this compilation starts.
168                            pass
169                        else:
170                            mods_and_submods_needed.append(modname)
171
172                for modname in mods_and_submods_needed:
173                    provider_src = self.provided_by[modname]
174                    provider_modfile = self.module_name_for(provider_src)
175                    # Prune self-dependencies
176                    if provider_src != src:
177                        module_files_needed.append(provider_modfile)
178
179                quoted_objfilename = ninja_quote(objfilename, True)
180                quoted_module_files_generated = [ninja_quote(x, True) for x in module_files_generated]
181                quoted_module_files_needed = [ninja_quote(x, True) for x in module_files_needed]
182                if quoted_module_files_generated:
183                    mod_gen = '| ' + ' '.join(quoted_module_files_generated)
184                else:
185                    mod_gen = ''
186                if quoted_module_files_needed:
187                    mod_dep = '| ' + ' '.join(quoted_module_files_needed)
188                else:
189                    mod_dep = ''
190                build_line = 'build {} {}: dyndep {}'.format(quoted_objfilename,
191                                                             mod_gen,
192                                                             mod_dep)
193                ofile.write(build_line + '\n')
194        return 0
195
196def run(args: T.List[str]) -> int:
197    assert len(args) == 3, 'got wrong number of arguments!'
198    pickle_file, outfile, jsonfile = args
199    with open(jsonfile, encoding='utf-8') as f:
200        sources = json.load(f)
201    scanner = DependencyScanner(pickle_file, outfile, sources)
202    return scanner.scan()
203