# Copyright 2015 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. import enum import os import re import typing as T from . import ExtensionModule from . import ModuleReturnValue from .. import mesonlib, build from .. import mlog from ..interpreter.type_checking import DEPEND_FILES_KW, DEPENDS_KW, INCLUDE_DIRECTORIES from ..interpreterbase.decorators import ContainerTypeInfo, FeatureNew, KwargInfo, typed_kwargs, typed_pos_args from ..mesonlib import MachineChoice, MesonException from ..programs import ExternalProgram if T.TYPE_CHECKING: from . import ModuleState from ..compilers import Compiler from ..interpreter import Interpreter from typing_extensions import TypedDict class CompileResources(TypedDict): depend_files: T.List[mesonlib.FileOrString] depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]] include_directories: T.List[T.Union[str, build.IncludeDirs]] args: T.List[str] class ResourceCompilerType(enum.Enum): windres = 1 rc = 2 wrc = 3 class WindowsModule(ExtensionModule): def __init__(self, interpreter: 'Interpreter'): super().__init__(interpreter) self._rescomp: T.Optional[T.Tuple[ExternalProgram, ResourceCompilerType]] = None self.methods.update({ 'compile_resources': self.compile_resources, }) def detect_compiler(self, compilers: T.Dict[str, 'Compiler']) -> 'Compiler': for l in ('c', 'cpp'): if l in compilers: return compilers[l] raise MesonException('Resource compilation requires a C or C++ compiler.') def _find_resource_compiler(self, state: 'ModuleState') -> T.Tuple[ExternalProgram, ResourceCompilerType]: # FIXME: Does not handle `native: true` executables, see # See https://github.com/mesonbuild/meson/issues/1531 # Take a parameter instead of the hardcoded definition below for_machine = MachineChoice.HOST if self._rescomp: return self._rescomp # Will try cross / native file and then env var rescomp = ExternalProgram.from_bin_list(state.environment, for_machine, 'windres') if not rescomp or not rescomp.found(): comp = self.detect_compiler(state.environment.coredata.compilers[for_machine]) if comp.id in {'msvc', 'clang-cl', 'intel-cl'}: rescomp = ExternalProgram('rc', silent=True) else: rescomp = ExternalProgram('windres', silent=True) if not rescomp.found(): raise MesonException('Could not find Windows resource compiler') for (arg, match, rc_type) in [ ('/?', '^.*Microsoft.*Resource Compiler.*$', ResourceCompilerType.rc), ('--version', '^.*GNU windres.*$', ResourceCompilerType.windres), ('--version', '^.*Wine Resource Compiler.*$', ResourceCompilerType.wrc), ]: p, o, e = mesonlib.Popen_safe(rescomp.get_command() + [arg]) m = re.search(match, o, re.MULTILINE) if m: mlog.log('Windows resource compiler: %s' % m.group()) self._rescomp = (rescomp, rc_type) break else: raise MesonException('Could not determine type of Windows resource compiler') return self._rescomp @typed_pos_args('windows.compile_resources', varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex), min_varargs=1) @typed_kwargs( 'winddows.compile_resoures', DEPEND_FILES_KW.evolve(since='0.47.0'), DEPENDS_KW.evolve(since='0.47.0'), INCLUDE_DIRECTORIES.evolve(name='include_directories'), KwargInfo('args', ContainerTypeInfo(list, str), default=[], listify=True), ) def compile_resources(self, state: 'ModuleState', args: T.Tuple[T.List[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]], kwargs: 'CompileResources') -> ModuleReturnValue: extra_args = kwargs['args'].copy() wrc_depend_files = kwargs['depend_files'] wrc_depends = kwargs['depends'] for d in wrc_depends: if isinstance(d, build.CustomTarget): extra_args += state.get_include_args([ build.IncludeDirs('', [], False, [os.path.join('@BUILD_ROOT@', self.interpreter.backend.get_target_dir(d))]) ]) extra_args += state.get_include_args(kwargs['include_directories']) rescomp, rescomp_type = self._find_resource_compiler(state) if rescomp_type == ResourceCompilerType.rc: # RC is used to generate .res files, a special binary resource # format, which can be passed directly to LINK (apparently LINK uses # CVTRES internally to convert this to a COFF object) suffix = 'res' res_args = extra_args + ['/nologo', '/fo@OUTPUT@', '@INPUT@'] elif rescomp_type == ResourceCompilerType.windres: # ld only supports object files, so windres is used to generate a # COFF object suffix = 'o' res_args = extra_args + ['@INPUT@', '@OUTPUT@'] m = 'Argument {!r} has a space which may not work with windres due to ' \ 'a MinGW bug: https://sourceware.org/bugzilla/show_bug.cgi?id=4933' for arg in extra_args: if ' ' in arg: mlog.warning(m.format(arg), fatal=False) else: suffix = 'o' res_args = extra_args + ['@INPUT@', '-o', '@OUTPUT@'] res_targets: T.List[build.CustomTarget] = [] def get_names() -> T.Iterable[T.Tuple[str, str, T.Union[str, mesonlib.File, build.CustomTargetIndex]]]: for src in args[0]: if isinstance(src, str): yield os.path.join(state.subdir, src), src, src elif isinstance(src, mesonlib.File): yield src.relative_name(), src.fname, src elif isinstance(src, build.CustomTargetIndex): FeatureNew.single_use('windows.compile_resource CustomTargetIndex in positional arguments', '0.61.0', state.subproject, location=state.current_node) # This dance avoids a case where two indexs of the same # target are given as separate arguments. yield (f'{src.get_id()}_{src.target.get_outputs().index(src.output)}', f'windows_compile_resources_{src.get_filename()}', src) else: if len(src.get_outputs()) > 1: FeatureNew.single_use('windows.compile_resource CustomTarget with multiple outputs in positional arguments', '0.61.0', state.subproject, location=state.current_node) for i, out in enumerate(src.get_outputs()): # Chances are that src.get_filename() is already the name of that # target, add a prefix to avoid name clash. yield f'{src.get_id()}_{i}', f'windows_compile_resources_{i}_{out}', src[i] for name, name_formatted, src in get_names(): # Path separators are not allowed in target names name = name.replace('/', '_').replace('\\', '_').replace(':', '_') name_formatted = name_formatted.replace('/', '_').replace('\\', '_').replace(':', '_') output = f'{name}_@BASENAME@.{suffix}' command: T.List[T.Union[str, ExternalProgram]] = [] command.append(rescomp) command.extend(res_args) res_kwargs = { 'output': output, 'input': [src], 'depend_files': wrc_depend_files, 'depends': wrc_depends, } # instruct binutils windres to generate a preprocessor depfile if rescomp_type == ResourceCompilerType.windres: res_kwargs['depfile'] = f'{output}.d' command.extend(['--preprocessor-arg=-MD', '--preprocessor-arg=-MQ@OUTPUT@', '--preprocessor-arg=-MF@DEPFILE@']) res_kwargs['command'] = command res_targets.append(build.CustomTarget(name_formatted, state.subdir, state.subproject, res_kwargs)) return ModuleReturnValue(res_targets, [res_targets]) def initialize(interp: 'Interpreter') -> WindowsModule: return WindowsModule(interp)