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