The Meson Build System
http://mesonbuild.com/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
9.3 KiB
206 lines
9.3 KiB
# Copyright 2020 The Meson development team |
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
|
# you may not use this file except in compliance with the License. |
|
# You may obtain a copy of the License at |
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0 |
|
|
|
# Unless required by applicable law or agreed to in writing, software |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
# See the License for the specific language governing permissions and |
|
# limitations under the License. |
|
from __future__ import annotations |
|
|
|
import json |
|
import os |
|
import pathlib |
|
import pickle |
|
import re |
|
import sys |
|
import typing as T |
|
|
|
from ..backend.ninjabackend import ninja_quote |
|
from ..compilers.compilers import lang_suffixes |
|
|
|
if T.TYPE_CHECKING: |
|
from ..backend.ninjabackend import TargetDependencyScannerInfo |
|
|
|
CPP_IMPORT_RE = re.compile(r'\w*import ([a-zA-Z0-9]+);') |
|
CPP_EXPORT_RE = re.compile(r'\w*export module ([a-zA-Z0-9]+);') |
|
|
|
FORTRAN_INCLUDE_PAT = r"^\s*include\s*['\"](\w+\.\w+)['\"]" |
|
FORTRAN_MODULE_PAT = r"^\s*\bmodule\b\s+(\w+)\s*(?:!+.*)*$" |
|
FORTRAN_SUBMOD_PAT = r"^\s*\bsubmodule\b\s*\((\w+:?\w+)\)\s*(\w+)" |
|
FORTRAN_USE_PAT = r"^\s*use,?\s*(?:non_intrinsic)?\s*(?:::)?\s*(\w+)" |
|
|
|
FORTRAN_MODULE_RE = re.compile(FORTRAN_MODULE_PAT, re.IGNORECASE) |
|
FORTRAN_SUBMOD_RE = re.compile(FORTRAN_SUBMOD_PAT, re.IGNORECASE) |
|
FORTRAN_USE_RE = re.compile(FORTRAN_USE_PAT, re.IGNORECASE) |
|
|
|
class DependencyScanner: |
|
def __init__(self, pickle_file: str, outfile: str, sources: T.List[str]): |
|
with open(pickle_file, 'rb') as pf: |
|
self.target_data: TargetDependencyScannerInfo = pickle.load(pf) |
|
self.outfile = outfile |
|
self.sources = sources |
|
self.provided_by: T.Dict[str, str] = {} |
|
self.exports: T.Dict[str, str] = {} |
|
self.needs: T.Dict[str, T.List[str]] = {} |
|
self.sources_with_exports: T.List[str] = [] |
|
|
|
def scan_file(self, fname: str) -> None: |
|
suffix = os.path.splitext(fname)[1][1:].lower() |
|
if suffix in lang_suffixes['fortran']: |
|
self.scan_fortran_file(fname) |
|
elif suffix in lang_suffixes['cpp']: |
|
self.scan_cpp_file(fname) |
|
else: |
|
sys.exit(f'Can not scan files with suffix .{suffix}.') |
|
|
|
def scan_fortran_file(self, fname: str) -> None: |
|
fpath = pathlib.Path(fname) |
|
modules_in_this_file = set() |
|
for line in fpath.read_text(encoding='utf-8').split('\n'): |
|
import_match = FORTRAN_USE_RE.match(line) |
|
export_match = FORTRAN_MODULE_RE.match(line) |
|
submodule_export_match = FORTRAN_SUBMOD_RE.match(line) |
|
if import_match: |
|
needed = import_match.group(1).lower() |
|
# In Fortran you have an using declaration also for the module |
|
# you define in the same file. Prevent circular dependencies. |
|
if needed not in modules_in_this_file: |
|
if fname in self.needs: |
|
self.needs[fname].append(needed) |
|
else: |
|
self.needs[fname] = [needed] |
|
if export_match: |
|
exported_module = export_match.group(1).lower() |
|
assert exported_module not in modules_in_this_file |
|
modules_in_this_file.add(exported_module) |
|
if exported_module in self.provided_by: |
|
raise RuntimeError(f'Multiple files provide module {exported_module}.') |
|
self.sources_with_exports.append(fname) |
|
self.provided_by[exported_module] = fname |
|
self.exports[fname] = exported_module |
|
if submodule_export_match: |
|
# Store submodule "Foo" "Bar" as "foo:bar". |
|
# A submodule declaration can be both an import and an export declaration: |
|
# |
|
# submodule (a1:a2) a3 |
|
# - requires a1@a2.smod |
|
# - produces a1@a3.smod |
|
parent_module_name_full = submodule_export_match.group(1).lower() |
|
parent_module_name = parent_module_name_full.split(':')[0] |
|
submodule_name = submodule_export_match.group(2).lower() |
|
concat_name = f'{parent_module_name}:{submodule_name}' |
|
self.sources_with_exports.append(fname) |
|
self.provided_by[concat_name] = fname |
|
self.exports[fname] = concat_name |
|
# Fortran requires that the immediate parent module must be built |
|
# before the current one. Thus: |
|
# |
|
# submodule (parent) parent <- requires parent.mod (really parent.smod, but they are created at the same time) |
|
# submodule (a1:a2) a3 <- requires a1@a2.smod |
|
# |
|
# a3 does not depend on the a1 parent module directly, only transitively. |
|
if fname in self.needs: |
|
self.needs[fname].append(parent_module_name_full) |
|
else: |
|
self.needs[fname] = [parent_module_name_full] |
|
|
|
def scan_cpp_file(self, fname: str) -> None: |
|
fpath = pathlib.Path(fname) |
|
for line in fpath.read_text(encoding='utf-8').split('\n'): |
|
import_match = CPP_IMPORT_RE.match(line) |
|
export_match = CPP_EXPORT_RE.match(line) |
|
if import_match: |
|
needed = import_match.group(1) |
|
if fname in self.needs: |
|
self.needs[fname].append(needed) |
|
else: |
|
self.needs[fname] = [needed] |
|
if export_match: |
|
exported_module = export_match.group(1) |
|
if exported_module in self.provided_by: |
|
raise RuntimeError(f'Multiple files provide module {exported_module}.') |
|
self.sources_with_exports.append(fname) |
|
self.provided_by[exported_module] = fname |
|
self.exports[fname] = exported_module |
|
|
|
def objname_for(self, src: str) -> str: |
|
objname = self.target_data.source2object[src] |
|
assert isinstance(objname, str) |
|
return objname |
|
|
|
def module_name_for(self, src: str) -> str: |
|
suffix = os.path.splitext(src)[1][1:].lower() |
|
if suffix in lang_suffixes['fortran']: |
|
exported = self.exports[src] |
|
# Module foo:bar goes to a file name foo@bar.smod |
|
# Module Foo goes to a file name foo.mod |
|
namebase = exported.replace(':', '@') |
|
if ':' in exported: |
|
extension = 'smod' |
|
else: |
|
extension = 'mod' |
|
return os.path.join(self.target_data.private_dir, f'{namebase}.{extension}') |
|
elif suffix in lang_suffixes['cpp']: |
|
return '{}.ifc'.format(self.exports[src]) |
|
else: |
|
raise RuntimeError('Unreachable code.') |
|
|
|
def scan(self) -> int: |
|
for s in self.sources: |
|
self.scan_file(s) |
|
with open(self.outfile, 'w', encoding='utf-8') as ofile: |
|
ofile.write('ninja_dyndep_version = 1\n') |
|
for src in self.sources: |
|
objfilename = self.objname_for(src) |
|
mods_and_submods_needed = [] |
|
module_files_generated = [] |
|
module_files_needed = [] |
|
if src in self.sources_with_exports: |
|
module_files_generated.append(self.module_name_for(src)) |
|
if src in self.needs: |
|
for modname in self.needs[src]: |
|
if modname not in self.provided_by: |
|
# Nothing provides this module, we assume that it |
|
# comes from a dependency library somewhere and is |
|
# already built by the time this compilation starts. |
|
pass |
|
else: |
|
mods_and_submods_needed.append(modname) |
|
|
|
for modname in mods_and_submods_needed: |
|
provider_src = self.provided_by[modname] |
|
provider_modfile = self.module_name_for(provider_src) |
|
# Prune self-dependencies |
|
if provider_src != src: |
|
module_files_needed.append(provider_modfile) |
|
|
|
quoted_objfilename = ninja_quote(objfilename, True) |
|
quoted_module_files_generated = [ninja_quote(x, True) for x in module_files_generated] |
|
quoted_module_files_needed = [ninja_quote(x, True) for x in module_files_needed] |
|
if quoted_module_files_generated: |
|
mod_gen = '| ' + ' '.join(quoted_module_files_generated) |
|
else: |
|
mod_gen = '' |
|
if quoted_module_files_needed: |
|
mod_dep = '| ' + ' '.join(quoted_module_files_needed) |
|
else: |
|
mod_dep = '' |
|
build_line = 'build {} {}: dyndep {}'.format(quoted_objfilename, |
|
mod_gen, |
|
mod_dep) |
|
ofile.write(build_line + '\n') |
|
return 0 |
|
|
|
def run(args: T.List[str]) -> int: |
|
assert len(args) == 3, 'got wrong number of arguments!' |
|
pickle_file, outfile, jsonfile = args |
|
with open(jsonfile, encoding='utf-8') as f: |
|
sources = json.load(f) |
|
scanner = DependencyScanner(pickle_file, outfile, sources) |
|
return scanner.scan()
|
|
|