xref: /aosp_15_r20/external/mesa3d/meson_to_hermetic/meson_to_hermetic.py (revision 6104692788411f58d303aa86923a9ff6ecaded22)
1"""
2Copyright 2024 Google LLC
3SPDX-License-Identifier: MIT
4"""
5
6import os
7import meson_impl as impl
8import tomllib
9import re
10
11from jinja2 import Environment, FileSystemLoader
12from pathlib import Path
13
14from meson_build_state import *
15
16jinja_env = Environment(
17    loader=FileSystemLoader(Path(__file__).parent.resolve() / 'templates/')
18)
19
20# A map that holds the build-system to build file
21# Keep the keys lower-case for non-case sensitivity
22OUTPUT_FILES = {'soong': r'Android_res.bp', 'bazel': r'BUILD.bazel'}
23
24
25def generate_build_file(translator, build_type: str):
26    defaults_gen = jinja_env.get_template('defaults/all_defaults.txt')
27    defaults = defaults_gen.render()
28    # Write our manually defined defaults
29    with open(OUTPUT_FILES[build_type], 'w') as file:
30        if build_type == 'soong':
31            file.write(defaults)
32        path = build_type + '/'
33        # Render all static libraries
34        static_libs_template = jinja_env.get_template(path + 'static_library.txt')
35        for static_lib in translator.meson_state.static_libraries:
36            if static_lib.library_type is LibraryType.LibraryShared:
37                static_libs_template = jinja_env.get_template(
38                    path + 'shared_library.txt'
39                )
40            cc_lib = static_libs_template.render(
41                name=static_lib.name,
42                host_supported='false',  # TODO(bpnguyen): Fix hardcoded host_supported
43                srcs=static_lib.srcs,
44                hdrs=static_lib.generated_headers,
45                generated_headers=static_lib.generated_headers,
46                generated_sources=static_lib.generated_sources,
47                copts=static_lib.copts,
48                c_std_val=static_lib.cstd,
49                cpp_std_val=static_lib.cpp_std,
50                cflags=static_lib.conlyflags,
51                cppflags=static_lib.cppflags,
52                include_directories=static_lib.local_include_dirs,
53                static_libs=static_lib.static_libs,
54                whole_static_libs=static_lib.whole_static_libs,
55                shared_libs=static_lib.shared_libs,
56                header_libs=static_lib.header_libs,
57                target_compatible_with=static_lib.target_compatible_with,
58                deps=static_lib.deps,
59            )
60            # Set the template back to static by default
61            if static_lib.library_type is LibraryType.LibraryShared:
62                static_libs_template = jinja_env.get_template(
63                    path + 'static_library.txt'
64                )
65            file.write(cc_lib)
66
67        # Render genrules / custom targets
68        custom_target_template = jinja_env.get_template(path + 'genrule.txt')
69        for custom_target in translator.meson_state.custom_targets:
70            genrule = custom_target_template.render(
71                name=custom_target.name,
72                srcs=custom_target.srcs,
73                outs=custom_target.out,
74                tools=custom_target.tools,
75                export=len(custom_target.export_include_dirs) > 0,
76                export_include_dirs=custom_target.export_include_dirs,
77                cmd=custom_target.cmd,
78            )
79            file.write(genrule)
80
81        # python binary hosts
82        py_binary_host_template = jinja_env.get_template(path + 'py_binary.txt')
83        for py_binary in translator.meson_state.custom_py_targets:
84            py_binary_render = py_binary_host_template.render(
85                name=py_binary.name,
86                main=py_binary.main,
87                srcs=py_binary.srcs,
88                imports=py_binary.imports,
89            )
90            file.write(py_binary_render)
91
92        include_dir_template = jinja_env.get_template(path + 'include_directories.txt')
93        for include_dir in translator.meson_state.include_dirs:
94            include_dir_render = include_dir_template.render(
95                name=include_dir.name,
96                hdrs=include_dir.dirs,
97            )
98            file.write(include_dir_render)
99
100
101class SoongGenerator(impl.Compiler):
102    def has_header_symbol(
103        self,
104        header: str,
105        symbol: str,
106        args=[],
107        dependencies=[],
108        include_directories=[],
109        no_builtin_args=False,
110        prefix=[],
111        required=False,
112    ):
113        # Exclude what is currently not working.
114        result = self.is_symbol_supported(header, symbol)
115        print("has_header_symbol '%s', '%s': %s" % (header, symbol, str(result)))
116        return result
117
118    def check_header(self, header, prefix=''):
119        result = self.is_header_supported(header)
120        print("check_header '%s': %s" % (header, str(result)))
121        return result
122
123    def has_function(self, function, args=[], prefix='', dependencies=''):
124        # Exclude what is currently not working.
125        result = self.is_function_supported(function)
126        print("has_function '%s': %s" % (function, str(result)))
127        return result
128
129    def links(self, snippet, name, args=[], dependencies=[]):
130        # Exclude what is currently not working.
131        result = self.is_link_supported(name)
132        print("links '%s': %s" % (name, str(result)))
133        return result
134
135    def generate(self, translator):
136        """
137        Generates a Soong valid build file
138        :return: None
139        """
140        # Render all the defaults to the file first.
141        generate_build_file(translator, 'soong')
142
143
144class BazelGenerator(impl.Compiler):
145    def is_symbol_supported(self, header: str, symbol: str):
146        if header in meson_translator.config.headers_not_supported:
147            return False
148        if symbol in meson_translator.config.symbols_not_supported:
149            return False
150        return super().is_symbol_supported(header, symbol)
151
152    def is_function_supported(self, function):
153        if function in meson_translator.config.functions_not_supported:
154            return False
155        return super().is_function_supported(function)
156
157    def is_link_supported(self, name):
158        if name in meson_translator.config.links_not_supported:
159            return False
160        return super().is_link_supported(name)
161
162    def has_header_symbol(
163        self,
164        header: str,
165        symbol: str,
166        args=None,
167        dependencies=None,
168        include_directories=None,
169        no_builtin_args=False,
170        prefix=None,
171        required=False,
172    ):
173        if args is None:
174            args = []
175        if dependencies is None:
176            dependencies = []
177        if include_directories is None:
178            include_directories = []
179        if prefix is None:
180            prefix = []
181        # Exclude what is currently not working.
182        result = self.is_symbol_supported(header, symbol)
183        print("has_header_symbol '%s', '%s': %s" % (header, symbol, str(result)))
184        return result
185
186    def check_header(self, header, prefix=''):
187        result = self.is_header_supported(header)
188        print(f"check_header '{header}': {result}")
189        return result
190
191    def has_function(self, function, args=[], prefix='', dependencies='') -> bool:
192        # Exclude what is currently not working.
193        result = self.is_function_supported(function)
194        print(f"has_function '{function}': {result}")
195        return result
196
197    def links(self, snippet, name, args=[], dependencies=[]):
198        # Exclude what is currently not working.
199        result = self.is_link_supported(name)
200        print(f"links '{name}': {result}")
201        return result
202
203    def generate(self, translator):
204        generate_build_file(translator, 'bazel')
205
206
207class BazelPkgConfigModule(impl.PkgConfigModule):
208    def generate(
209        self,
210        lib,
211        name='',
212        description='',
213        extra_cflags=None,
214        filebase='',
215        version='',
216        libraries=None,
217        libraries_private=None,
218    ):
219        if extra_cflags is None:
220            extra_cflags = []
221        impl.fprint('# package library')
222        impl.fprint('cc_library(')
223        impl.fprint('  name = "%s",' % name)
224        assert type(lib) is impl.StaticLibrary
225        impl.fprint('  deps = [ "%s" ],' % lib.target_name)
226        # This line tells Bazel to use -isystem for targets that depend on this one,
227        # which is needed for clients that include package headers with angle brackets.
228        impl.fprint('  includes = [ "." ],')
229        impl.fprint('  visibility = [ "//visibility:public" ],')
230        impl.fprint(')')
231
232
233class MesonTranslator:
234    """
235    Abstract class that defines all common attributes
236    between different build systems (Soong, Bazel, etc...)
237    """
238
239    def __init__(self):
240        self._generator = None
241        self._config_file = None
242        self._build = ''
243        # configs and meson_project_states are ordered with each other.
244        self._configs: list[ProjectConfig] = []
245        self._meson_project_states: list[MesonProjectState] = []
246        # TODO(357080225): Fix the use hardcoded state 0
247        self._state = 0  # index of self._configs
248
249    def init_data(self, config_file: str):
250        self._config_file: str = config_file
251        print('CONFIG:', self._config_file)
252        self._init_metadata()
253        _generator = (
254            SoongGenerator(self.config.cpu_family)
255            if self._build.lower() == 'soong'
256            else BazelGenerator(self.config.cpu_family)
257        )
258        self._generator: impl.Compiler = _generator
259        return self
260
261    @property
262    def config_file(self) -> Path:
263        return self._config_file
264
265    @property
266    def host_machine(self) -> str:
267        return self._configs[self._state].host_machine
268
269    @property
270    def build_machine(self) -> str:
271        return self._configs[self._state].build_machine
272
273    @property
274    def generator(self) -> impl.Compiler:
275        return self._generator
276
277    @property
278    def build(self) -> str:
279        return self._build
280
281    @property
282    def config(self) -> ProjectConfig:
283        return self._configs[self._state]
284
285    @property
286    def meson_state(self):
287        return self._meson_project_states[self._state]
288
289    def is_soong(self):
290        return self._build.lower() == 'soong'
291
292    def is_bazel(self):
293        return self._build.lower() == 'bazel'
294
295    def get_output_filename(self) -> str:
296        return OUTPUT_FILES[self._build.lower()]
297
298    def _init_metadata(self):
299        """
300        To be called after self._config_file has alredy been assigned.
301        Parses the given config files for common build attributes
302        Fills the list of all project configs
303        :return: None
304        """
305        with open(self._config_file, 'rb') as f:
306            data = tomllib.load(f)
307            self._build = data.get('build')
308            base_config = data.get('base_project_config')
309
310            configs = data.get('project_config')
311            for config in configs:
312                proj_config = ProjectConfig.create_project_config(self._build, **config)
313                self._configs.append(
314                    proj_config
315                )
316                self._meson_project_states.append(MesonProjectState())
317
318            new_configs = []
319            # Handle Inheritance
320            for config in self._configs:
321                # Parent config, that contains shared attributes
322                base_proj_config = ProjectConfig.create_project_config(self._build, **base_config)
323                if not config.inherits_from:
324                    new_configs.append(config)
325                    continue
326                if config.inherits_from == 'base_project_config':
327                    new_configs.append(
328                        base_proj_config.extend(config).deepcopy()
329                    )
330                else:
331                    for proj_config in self._configs:
332                        if config.inherits_from == proj_config.name:
333                            new_configs.append(
334                                proj_config.extend(config).deepcopy()
335                            )
336            self._configs = new_configs
337
338
339# Declares an empty attribute data class
340# metadata is allocated during Generators-runtime (I.E. generate_<host>_build.py)
341meson_translator = MesonTranslator()
342
343_gIncludeDirectories = {}
344
345
346def load_meson_data(config_file: str):
347    meson_translator.init_data(config_file)
348
349
350def open_output_file():
351    impl.open_output_file(meson_translator.get_output_filename())
352
353
354def close_output_file():
355    impl.close_output_file()
356
357
358def add_subdirs_to_set(dir_, dir_set):
359    subdirs = os.listdir(dir_)
360    for subdir in subdirs:
361        subdir = os.path.join(dir_, subdir)
362        if os.path.isdir(subdir):
363            dir_set.add(subdir)
364
365
366def include_directories(*paths, is_system=False):
367    if meson_translator.host_machine == 'android':
368        return impl.IncludeDirectories('', impl.get_include_dirs(paths))
369    # elif host_machine == 'fuchsia'
370    dirs = impl.get_include_dirs(paths)
371    name = dirs[0].replace('/', '_')
372    if is_system:
373        name += '_sys'
374
375    global _gIncludeDirectories
376    if name not in _gIncludeDirectories:
377        # Mesa likes to include files at a level down from the include path,
378        # so ensure Bazel allows this by including all of the possibilities.
379        # Can't repeat entries so use a set.
380        dir_set = set()
381        for dir_ in dirs:
382            dir_ = os.path.normpath(dir_)
383            dir_set.add(dir_)
384            add_subdirs_to_set(dir_, dir_set)
385
386        # HACK: For special cases we go down two levels:
387        # - Mesa vulkan runtime does #include "vulkan/wsi/..."
388        paths_needing_two_levels = ['src/vulkan']
389        for dir_ in list(dir_set):
390            if dir_ in paths_needing_two_levels:
391                add_subdirs_to_set(dir_, dir_set)
392
393        include_dir = IncludeDirectories()
394        include_dir.name = name
395
396        for dir_ in dir_set:
397            include_dir.dirs.append(os.path.normpath(os.path.join(dir_, '*.h')))
398            include_dir.dirs.append(os.path.normpath(os.path.join(dir_, '*.c')))
399        include_dir.visibility.append('//visibility:public')
400        _gIncludeDirectories[name] = impl.IncludeDirectories(name, dirs)
401        meson_translator.meson_state.include_dirs.append(include_dir)
402    return _gIncludeDirectories[name]
403
404
405def module_import(name: str):
406    if name == 'python':
407        return impl.PythonModule()
408    if name == 'pkgconfig' and meson_translator.host_machine.lower() == 'fuchsia':
409        return BazelPkgConfigModule()
410    if name == 'pkgconfig' and meson_translator.host_machine.lower() == 'android':
411        return impl.PkgConfigModule()
412    if name == 'pkgconfig' and meson_translator.host_machine.lower() == 'linux':
413        return impl.PkgConfigModule()
414    exit(
415        f'Unhandled module: "{name}" for host machine: "{meson_translator.host_machine}"'
416    )
417
418
419def load_dependencies():
420    impl.load_dependencies(meson_translator.config_file)
421
422
423def dependency(
424    *names,
425    required=True,
426    version='',
427    allow_fallback=False,
428    method='',
429    modules=[],
430    optional_modules=[],
431    static=True,
432    fallback=[],
433    include_type='',
434    native=False,
435):
436    return impl.dependency(*names, required=required, version=version)
437
438
439def static_library(
440    target_name,
441    *source_files,
442    c_args=[],
443    cpp_args=[],
444    c_pch='',
445    build_by_default=False,
446    build_rpath='',
447    d_debug=[],
448    d_import_dirs=[],
449    d_module_versions=[],
450    d_unittest=False,
451    dependencies=[],
452    extra_files='',
453    gnu_symbol_visibility='',
454    gui_app=False,
455    implicit_include_directories=False,
456    include_directories=[],
457    install=False,
458    install_dir='',
459    install_mode=[],
460    install_rpath='',
461    install_tag='',
462    link_args=[],
463    link_depends='',
464    link_language='',
465    link_whole=[],
466    link_with=[],
467    name_prefix='',
468    name_suffix='',
469    native=False,
470    objects=[],
471    override_options=[],
472    pic=False,
473    prelink=False,
474    rust_abi='',
475    rust_crate_type='',
476    rust_dependency_map={},
477    sources='',
478    vala_args=[],
479    win_subsystem='',
480):
481    print('static_library: ' + target_name)
482
483    link_with = impl.get_linear_list(link_with)
484    link_whole = impl.get_linear_list(link_whole)
485
486    # Emitting link_with/link_whole into a static library isn't useful for building,
487    # shared libraries must specify the whole chain of dependencies.
488    # Leaving them here for traceability though maybe it's less confusing to remove them?
489    _emit_builtin_target(
490        target_name,
491        *source_files,
492        c_args=c_args,
493        cpp_args=cpp_args,
494        dependencies=dependencies,
495        include_directories=include_directories,
496        link_with=link_with,
497        link_whole=link_whole,
498        builtin_type_name='cc_library_static',
499        static=meson_translator.is_bazel(),
500        library_type=LibraryType.LibraryStatic,
501    )
502
503    return impl.StaticLibrary(target_name, link_with=link_with, link_whole=link_whole)
504
505
506def _get_sources(input_sources, sources, generated_sources, generated_headers):
507    def generate_sources_fuchsia(source_):
508        if type(source_) is impl.CustomTarget:
509            generated_sources.add(source_.target_name())
510            for out in source_.outputs:
511                sources.add(out)
512        elif type(source_) is impl.CustomTargetItem:
513            target = source_.target
514            generated_sources.add(target.target_name())
515            sources.add(target.outputs[source_.index])
516
517    def generate_sources_android(source_):
518        if type(source_) is impl.CustomTarget:
519            if source_.generates_h():
520                generated_headers.add(source_.target_name_h())
521            if source_.generates_c():
522                generated_sources.add(source_.target_name_c())
523        elif type(source_) is impl.CustomTargetItem:
524            source_ = source_.target
525            if source_.generates_h():
526                generated_headers.add(source_.target_name_h())
527            if source_.generates_c():
528                generated_sources.add(source_.target_name_c())
529
530    for source in input_sources:
531        if type(source) is list:
532            _get_sources(source, sources, generated_sources, generated_headers)
533        elif type(source) is impl.CustomTarget or type(source) is impl.CustomTargetItem:
534            if meson_translator.is_bazel():
535                generate_sources_fuchsia(source)
536            else:  # is_soong
537                generate_sources_android(source)
538        elif type(source) is impl.File:
539            sources.add(source.name)
540        elif type(source) is str:
541            sources.add(impl.get_relative_dir(source))
542        else:
543            exit(f'source type not handled: {type(source)}')
544
545
546# TODO(bpnguyen): Implement some bazel/soong parser that would merge
547# logic from this function to _emit_builtin_target_android
548def _emit_builtin_target_fuchsia(
549    target_name,
550    *source,
551    static=False,
552    c_args=[],
553    cpp_args=[],
554    dependencies=[],
555    include_directories=[],
556    link_with=[],
557    link_whole=[],
558    library_type=LibraryType.Library,
559):
560    sl = StaticLibrary()
561    target_name_so = target_name
562    target_name = target_name if static else '_' + target_name
563    sl.name = target_name
564    sl.library_type = library_type
565
566    srcs = set()
567    generated_sources = set()
568    generated_headers = set()
569    generated_header_files = []
570    for source_arg in source:
571        assert type(source_arg) is list
572        _get_sources(source_arg, srcs, generated_sources, generated_headers)
573
574    deps = impl.get_set_of_deps(dependencies)
575    include_directories = impl.get_include_directories(include_directories)
576    static_libs = []
577    whole_static_libs = []
578    shared_libs = []
579    for dep in deps:
580        print('  dep: ' + dep.name)
581        for src in impl.get_linear_list([dep.sources]):
582            if type(src) is impl.CustomTargetItem:
583                generated_headers.add(src.target.target_name_h())
584                generated_header_files.extend(src.target.header_outputs())
585            elif type(src) is impl.CustomTarget:
586                generated_headers.add(src.target_name_h())
587                generated_header_files.extend(src.header_outputs())
588            else:
589                exit('Unhandled source dependency: ' + str(type(src)))
590        include_directories.extend(
591            impl.get_include_directories(dep.include_directories)
592        )
593        for target in impl.get_static_libs([dep.link_with]):
594            assert type(target) is impl.StaticLibrary
595            static_libs.append(target.target_name)
596        for target in impl.get_linear_list([dep.link_whole]):
597            assert type(target) is impl.StaticLibrary
598            whole_static_libs.append(target.target_name)
599        for target in dep.targets:
600            if target.target_type == impl.DependencyTargetType.SHARED_LIBRARY:
601                shared_libs.append(target.target_name)
602            elif target.target_type == impl.DependencyTargetType.STATIC_LIBRARY:
603                static_libs.append(target.target_name)
604            elif target.target_type == impl.DependencyTargetType.HEADER_LIBRARY:
605                exit('Header library not supported')
606        c_args.append(dep.compile_args)
607        cpp_args.append(dep.compile_args)
608
609    for target in impl.get_static_libs(link_with):
610        if type(target) is impl.StaticLibrary:
611            static_libs.append(target.target_name)
612        else:
613            exit('Unhandled link_with type: ' + str(type(target)))
614
615    for target in impl.get_whole_static_libs(link_whole):
616        if type(target) is impl.StaticLibrary:
617            whole_static_libs.append(target.target_name())
618        else:
619            exit('Unhandled link_whole type: ' + str(type(target)))
620
621    # Android turns all warnings into errors but thirdparty projects typically can't handle that
622    cflags = ['-Wno-error'] + impl.get_linear_list(impl.get_project_cflags() + c_args)
623    cppflags = ['-Wno-error'] + impl.get_linear_list(
624        impl.get_project_cppflags() + cpp_args
625    )
626
627    has_c_files = False
628    for src in srcs:
629        if src.endswith('.c'):
630            has_c_files = True
631        sl.srcs.append(src)
632
633    # For Bazel to find headers in the "current area", we have to include
634    # not just the headers in the relative dir, but also relative subdirs
635    # that aren't themselves areas (containing meson.build).
636    # We only look one level deep.
637    local_include_dirs = [impl.get_relative_dir()]
638    local_entries = (
639        [] if impl.get_relative_dir() == '' else os.listdir(impl.get_relative_dir())
640    )
641    for entry in local_entries:
642        entry = os.path.join(impl.get_relative_dir(), entry)
643        if os.path.isdir(entry):
644            subdir_entries = os.listdir(entry)
645            if 'meson.build' not in subdir_entries:
646                local_include_dirs.append(entry)
647
648    for hdr in set(generated_header_files):
649        sl.generated_headers.append(hdr)
650    for hdr in local_include_dirs:
651        sl.local_include_dirs.append(os.path.normpath(os.path.join(hdr, '*.h')))
652    # Needed for subdir sources
653    sl.copts.append(f'-I {impl.get_relative_dir()}')
654    sl.copts.append(f'-I $(GENDIR)/{impl.get_relative_dir()}')
655    for inc in include_directories:
656        for dir in inc.dirs:
657            sl.copts.append(f'-I {dir}')
658            sl.copts.append(f'-I $(GENDIR)/{dir}')
659
660    if has_c_files:
661        for arg in cflags:
662            # Double escape double quotations
663            arg = re.sub(r'"', '\\\\\\"', arg)
664            sl.copts.append(arg)
665    else:
666        for arg in cppflags:
667            # Double escape double quotations
668            arg = re.sub(r'"', '\\\\\\"', arg)
669            sl.copts.append(arg)
670
671    # Ensure bazel deps are unique
672    bazel_deps = set()
673    for lib in static_libs:
674        bazel_deps.add(lib)
675    for lib in whole_static_libs:
676        bazel_deps.add(lib)
677    for inc in include_directories:
678        bazel_deps.add(inc.name)
679    for target in generated_headers:
680        bazel_deps.add(target)
681    for target in generated_sources:
682        bazel_deps.add(target)
683
684    for bdep in bazel_deps:
685        sl.deps.append(bdep)
686
687    sl.target_compatible_with.append('@platforms//os:fuchsia')
688    sl.visibility.append('//visibility:public')
689
690    meson_translator.meson_state.static_libraries.append(sl)
691
692    if not static:
693        shared_sl = StaticLibrary()
694        shared_sl.library_type = LibraryType.LibraryShared
695        shared_sl.name = target_name_so
696        shared_sl.deps.append(target_name)
697        meson_translator.meson_state.static_libraries.append(shared_sl)
698
699
700# TODO(bpnguyen): Implement some bazel/soong parser that would merge
701# logic from this function to _emit_builtin_target_fuchsia
702def _emit_builtin_target_android(
703    target_name,
704    *source,
705    c_args=[],
706    cpp_args=[],
707    dependencies=[],
708    include_directories=[],
709    link_with=[],
710    link_whole=[],
711    builtin_type_name='',
712    static=False,
713    library_type=LibraryType.Library,
714):
715    static_lib = StaticLibrary()
716    static_lib.name = target_name
717    static_lib.library_type = library_type
718
719    srcs = set()
720    generated_sources = set()
721    generated_headers = set()
722    for source_arg in source:
723        assert type(source_arg) is list
724        _get_sources(source_arg, srcs, generated_sources, generated_headers)
725
726    deps = impl.get_set_of_deps(dependencies)
727    include_directories = [impl.get_relative_dir()] + impl.get_include_dirs(
728        include_directories
729    )
730    static_libs = []
731    whole_static_libs = []
732    shared_libs = []
733    header_libs = []
734    for dep in deps:
735        print('  dep: ' + dep.name)
736        for src in impl.get_linear_list([dep.sources]):
737            if type(src) is impl.CustomTargetItem:
738                generated_headers.add(src.target.target_name_h())
739            elif type(src) is impl.CustomTarget:
740                generated_headers.add(src.target_name_h())
741            else:
742                exit('Unhandled source dependency: ' + str(type(src)))
743        include_directories.extend(impl.get_include_dirs(dep.include_directories))
744        for target in impl.get_static_libs([dep.link_with]):
745            assert type(target) is impl.StaticLibrary
746            static_libs.append(target.target_name)
747        for target in impl.get_linear_list([dep.link_whole]):
748            assert type(target) is impl.StaticLibrary
749            whole_static_libs.append(target.target_name)
750        for target in dep.targets:
751            if target.target_type is impl.DependencyTargetType.SHARED_LIBRARY:
752                shared_libs.append(target.target_name)
753            elif target.target_type is impl.DependencyTargetType.STATIC_LIBRARY:
754                static_libs.append(target.target_name)
755            elif target.target_type is impl.DependencyTargetType.HEADER_LIBRARY:
756                header_libs.append(target.target_name)
757        c_args.append(dep.compile_args)
758        cpp_args.append(dep.compile_args)
759
760    for target in impl.get_static_libs(link_with):
761        if type(target) is impl.StaticLibrary:
762            static_libs.append(target.target_name)
763        else:
764            exit(f'Unhandled link_with type: {type(target)}')
765
766    for target in impl.get_whole_static_libs(link_whole):
767        if type(target) is impl.StaticLibrary:
768            whole_static_libs.append(target.target_name())
769        else:
770            exit(f'Unhandled link_whole type: {type(target)}')
771
772    # Android turns all warnings into errors but thirdparty projects typically can't handle that
773    cflags = ['-Wno-error'] + impl.get_linear_list(impl.get_project_cflags() + c_args)
774    cppflags = ['-Wno-error'] + impl.get_linear_list(
775        impl.get_project_cppflags() + cpp_args
776    )
777
778    for src in srcs:
779        # Filter out header files
780        if not src.endswith('.h'):
781            static_lib.srcs.append(src)
782    for generated in generated_headers:
783        static_lib.generated_headers.append(generated)
784    for generated in generated_sources:
785        static_lib.generated_sources.append(generated)
786
787    for arg in impl.get_project_options():
788        if arg.name == 'c_std':
789            static_lib.cstd = arg.value
790        elif arg.name == 'cpp_std':
791            static_lib.cpp_std = arg.value
792
793    for arg in cflags:
794        # Escape double quotations
795        arg = re.sub(r'"', '\\"', arg)
796        static_lib.conlyflags.append(arg)
797    for arg in cppflags:
798        # Escape double quotations
799        arg = re.sub(r'"', '\\"', arg)
800        static_lib.cppflags.append(arg)
801
802    for inc in include_directories:
803        static_lib.local_include_dirs.append(inc)
804
805    for lib in static_libs:
806        static_lib.static_libs.append(lib)
807
808    for lib in whole_static_libs:
809        static_lib.whole_static_libs.append(lib)
810
811    for lib in shared_libs:
812        static_lib.shared_libs.append(lib)
813
814    for lib in header_libs:
815        static_lib.header_libs.append(lib)
816
817    meson_translator.meson_state.static_libraries.append(static_lib)
818
819
820def _emit_builtin_target(
821    target_name,
822    *source,
823    c_args=[],
824    cpp_args=[],
825    dependencies=[],
826    include_directories=[],
827    link_with=[],
828    link_whole=[],
829    builtin_type_name='',
830    static=False,
831    library_type=LibraryType.Library,
832):
833    if meson_translator.is_bazel():
834        _emit_builtin_target_fuchsia(
835            target_name,
836            *source,
837            c_args=c_args,
838            cpp_args=cpp_args,
839            dependencies=dependencies,
840            include_directories=include_directories,
841            link_with=link_with,
842            link_whole=link_whole,
843            static=static,
844            library_type=library_type,
845        )
846    else:  # meson_translator.is_soong()
847        _emit_builtin_target_android(
848            target_name,
849            *source,
850            c_args=c_args,
851            cpp_args=cpp_args,
852            dependencies=dependencies,
853            include_directories=include_directories,
854            link_with=link_with,
855            link_whole=link_whole,
856            builtin_type_name=builtin_type_name,
857            library_type=library_type,
858        )
859
860
861def shared_library(
862    target_name,
863    *source,
864    c_args=[],
865    cpp_args=[],
866    c_pch='',
867    build_by_default=False,
868    build_rpath='',
869    d_debug=[],
870    d_import_dirs=[],
871    d_module_versions=[],
872    d_unittest=False,
873    darwin_versions='',
874    dependencies=[],
875    extra_files='',
876    gnu_symbol_visibility='',
877    gui_app=False,
878    implicit_include_directories=False,
879    include_directories=[],
880    install=False,
881    install_dir='',
882    install_mode=[],
883    install_rpath='',
884    install_tag='',
885    link_args=[],
886    link_depends=[],
887    link_language='',
888    link_whole=[],
889    link_with=[],
890    name_prefix='',
891    name_suffix='',
892    native=False,
893    objects=[],
894    override_options=[],
895    rust_abi='',
896    rust_crate_type='',
897    rust_dependency_map={},
898    sources=[],
899    soversion='',
900    vala_args=[],
901    version='',
902    vs_module_defs='',
903    win_subsystem='',
904):
905    print('shared_library: ' + target_name)
906
907    link_with = impl.get_linear_list([link_with])
908    link_whole = impl.get_linear_list([link_whole])
909
910    _emit_builtin_target(
911        target_name,
912        *source,
913        c_args=c_args,
914        static=False,
915        cpp_args=cpp_args,
916        dependencies=dependencies,
917        include_directories=include_directories,
918        link_with=link_with,
919        link_whole=link_whole,
920        builtin_type_name='cc_library_shared',
921        library_type=LibraryType.LibraryStatic,
922    )
923    return impl.SharedLibrary(target_name)
924
925
926def _process_target_name(name):
927    name = re.sub(r'[\[\]]', '', name)
928    return name
929
930
931def _location_wrapper(name_or_list) -> str | list[str]:
932    if isinstance(name_or_list, list):
933        ret = []
934        for i in name_or_list:
935            ret.append(f'$(location {i})')
936        return ret
937
938    assert isinstance(name_or_list, str)
939    return f'$(location {name_or_list})'
940
941
942def _is_header(name):
943    return re.search(r'\.h[xx|pp]?$', name) is not None
944
945
946def _is_source(name):
947    return re.search(r'\.c[c|xx|pp]?$', name) is not None
948
949
950def _get_command_args(
951    command,
952    input,
953    output,
954    deps,
955    location_wrap=False,
956    obfuscate_output_c=False,
957    obfuscate_output_h=False,
958    obfuscate_suffix='',
959):
960    args = []
961    gendir = 'GENDIR' if meson_translator.is_bazel() else 'genDir'
962    for command_item in command[1:]:
963        if isinstance(command_item, list):
964            for item in command_item:
965                if type(item) is impl.File:
966                    args.append(
967                        _location_wrapper(item.name) if location_wrap else item.name
968                    )
969                elif type(item) is str:
970                    args.append(item)
971            continue
972
973        assert type(command_item) is str
974        match = re.match(r'@INPUT([0-9])?@', command_item)
975        if match is not None:
976            if match.group(1) is not None:
977                input_index = int(match.group(1))
978                input_list = impl.get_list_of_relative_inputs(input[input_index])
979            else:
980                input_list = impl.get_list_of_relative_inputs(input)
981            args.extend(_location_wrapper(input_list) if location_wrap else input_list)
982            continue
983
984        match = re.match(r'(.*?)@OUTPUT([0-9])?@', command_item)
985        if match is not None:
986            output_list = []
987            if match.group(2) is not None:
988                output_index = int(match.group(2))
989                selected_output = (
990                    output[output_index] if isinstance(output, list) else output
991                )
992                output_list.append(impl.get_relative_gen_dir(selected_output))
993            elif isinstance(output, list):
994                for out in output:
995                    output_list.append(impl.get_relative_gen_dir(out))
996            else:
997                output_list.append(impl.get_relative_gen_dir(output))
998            for out in output_list:
999                if _is_header(out) and obfuscate_output_h:
1000                    args.append(
1001                        match.group(1) + f'$({gendir})/{out}' if location_wrap else out
1002                    )
1003                else:
1004                    if _is_source(out) and obfuscate_output_c:
1005                        out += obfuscate_suffix
1006                    args.append(
1007                        match.group(1) + _location_wrapper(out)
1008                        if location_wrap
1009                        else out
1010                    )
1011            continue
1012
1013        # Assume used to locate generated outputs
1014        match = re.match(r'(.*?)@CURRENT_BUILD_DIR@', command_item)
1015        if match is not None:
1016            args.append(f'$({gendir})' + '/' + impl.get_relative_dir())
1017            continue
1018
1019        if meson_translator.is_bazel():
1020            match = re.match(r'@PROJECT_BUILD_ROOT@(.*)', command_item)
1021            if match is not None:
1022                args.append(f'$({gendir}){match.group(1)}')
1023                continue
1024
1025        # A plain arg
1026        if ' ' in command_item:
1027            args.append(f"'{command_item}'")
1028        else:
1029            args.append(command_item)
1030
1031    return args
1032
1033
1034def library(
1035    target_name,
1036    *sources,
1037    c_args=[],
1038    install=False,
1039    link_args=[],
1040    vs_module_defs='',
1041    version='',
1042):
1043    print('library: ' + target_name)
1044
1045    return static_library(
1046        target_name,
1047        *sources,
1048        c_args=c_args,
1049        install=install,
1050        link_args=link_args,
1051        sources=sources,
1052    )
1053
1054
1055# Assume dependencies of custom targets are custom targets that are generating
1056# python scripts; build a python path of their locations.
1057def _get_python_path(deps):
1058    python_path = ''
1059    for index, dep in enumerate(deps):
1060        assert type(dep) is impl.CustomTarget
1061        if index > 0:
1062            python_path += ':'
1063        python_path += '`dirname %s`' % _location_wrapper(':%s' % dep.target_name())
1064    return python_path
1065
1066
1067def _get_export_include_dirs():
1068    dirs = [impl.get_relative_dir()]
1069    # HACK for source files that expect that include generated files like:
1070    # include "vulkan/runtime/...h"
1071    if impl.get_relative_dir().startswith('src'):
1072        dirs.append('src')
1073    return dirs
1074
1075
1076def _process_wrapped_args_for_python(
1077    wrapped_args, python_script, python_script_target_name, deps
1078):
1079    # The python script arg should be replaced with the python binary target name
1080    args = impl.replace_wrapped_input_with_target(
1081        wrapped_args, python_script, python_script_target_name
1082    )
1083    if meson_translator.is_bazel():
1084        return args
1085    # else is_soong():
1086    python_path = 'PYTHONPATH='
1087    # Python scripts expect to be able to import other scripts from the same directory, but this
1088    # doesn't work in the soong execution environment, so we have to explicitly add the script
1089    # dir. We can't use $(location python_binary) because that is missing the relative path;
1090    # instead we can use $(location python_script), which happens to work, and we're careful to
1091    # ensure the script is in the list of sources even when it's used as the command directly.
1092    python_path += '`dirname $(location %s)`' % python_script
1093    # Also ensure that scripts generated by dependent custom targets can be imported.
1094    if type(deps) is impl.CustomTarget:
1095        python_path += ':' + _get_python_path([deps])
1096    if type(deps) is list:
1097        python_path += ':' + _get_python_path(deps)
1098    args.insert(0, python_path)
1099    return args
1100
1101
1102# TODO(bpnguyen): merge custom_target
1103def custom_target(
1104    target_name: str,
1105    build_always=False,
1106    build_always_stale=False,
1107    build_by_default=False,
1108    capture=False,
1109    command=[],
1110    console=False,
1111    depend_files=[],
1112    depends=[],
1113    depfile='',
1114    env=[],
1115    feed=False,
1116    input=[],
1117    install=False,
1118    install_dir='',
1119    install_mode=[],
1120    install_tag=[],
1121    output=[],
1122):
1123    target_name = _process_target_name(target_name)
1124    print('Custom target: ' + target_name)
1125    assert type(command) is list
1126    program = command[0]
1127    program_args = []
1128
1129    # The program can be an array that includes arguments
1130    if isinstance(program, list):
1131        for arg in program[1:]:
1132            assert type(arg) is str
1133            program_args.append(arg)
1134        program = program[0]
1135
1136    assert isinstance(program, impl.Program)
1137    assert program.found()
1138
1139    args = program_args + _get_command_args(command, input, output, depends)
1140
1141    # Python scripts need special handling to find mako library
1142    python_script = ''
1143    python_script_target_name = ''
1144    if program.command.endswith('.py'):
1145        python_script = program.command
1146    else:
1147        for index, arg in enumerate(args):
1148            if arg.endswith('.py'):
1149                python_script = arg
1150                break
1151    if python_script != '':
1152        python_script_target_name = target_name + '_' + os.path.basename(python_script)
1153        srcs = [python_script] + impl.get_list_of_relative_inputs(depend_files)
1154        python_custom_target = PythonCustomTarget()
1155        python_custom_target.name = python_script_target_name
1156        python_custom_target.main = python_script
1157        for src in srcs:
1158            if src.endswith('.py'):
1159                python_custom_target.srcs.append(src)
1160        for src in set(srcs):
1161            if src.endswith('.py'):
1162                python_custom_target.imports.append(os.path.dirname(src))
1163
1164        meson_translator.meson_state.custom_py_targets.append(python_custom_target)
1165
1166    relative_inputs = impl.get_list_of_relative_inputs(input)
1167    # We use python_host_binary instead of calling python scripts directly;
1168    # however there's an issue with python locating modules in the same directory
1169    # as the script; to workaround that (see _process_wrapped_args_for_python) we
1170    # ensure the script is listed in the genrule targets.
1171    if python_script != '' and python_script not in relative_inputs:
1172        relative_inputs.append(python_script)
1173    relative_inputs.extend(impl.get_list_of_relative_inputs(depend_files))
1174
1175    relative_inputs_set = set()
1176    if meson_translator.is_soong():
1177        for src in relative_inputs:
1178            relative_inputs_set.add(src)
1179
1180    relative_outputs = []
1181    if isinstance(output, list):
1182        for file in output:
1183            relative_outputs.append(impl.get_relative_gen_dir(file))
1184    else:
1185        assert type(output) is str
1186        relative_outputs.append(impl.get_relative_gen_dir(output))
1187
1188    generates_h = False
1189    generates_c = False
1190    custom_target_ = None
1191    if meson_translator.is_soong():
1192        # Soong requires genrule to generate only headers OR non-headers
1193        for out in relative_outputs:
1194            if _is_header(out):
1195                generates_h = True
1196            if _is_source(out):
1197                generates_c = True
1198        custom_target_ = impl.CustomTarget(
1199            target_name, relative_outputs, generates_h, generates_c
1200        )
1201    else:  # is_bazel:
1202        custom_target_ = impl.CustomTarget(target_name, relative_outputs)
1203
1204    program_command = program.command
1205    if meson_translator.is_soong():
1206        if program_command == 'bison':
1207            program_command_arg = 'M4=$(location m4) $(location bison)'
1208        elif program_command == 'flex':
1209            program_command_arg = 'M4=$(location m4) $(location flex)'
1210        elif program_command.endswith('.py'):
1211            program_command_arg = _location_wrapper(program_command)
1212        else:
1213            program_command_arg = program_command
1214
1215        program_args = [program_command_arg] + program_args
1216
1217        if custom_target_.generates_h() and custom_target_.generates_c():
1218            # Make a rule for only the headers
1219            obfuscate_suffix = '.dummy.h'
1220            wrapped_args = program_args + _get_command_args(
1221                command,
1222                input,
1223                output,
1224                depends,
1225                location_wrap=True,
1226                obfuscate_output_c=True,
1227                obfuscate_suffix=obfuscate_suffix,
1228            )
1229            if python_script:
1230                wrapped_args = _process_wrapped_args_for_python(
1231                    wrapped_args, python_script, python_script_target_name, depends
1232                )
1233
1234            command_line = impl.get_command_line_from_args(wrapped_args)
1235            if capture:
1236                command_line += ' > %s' % _location_wrapper(
1237                    impl.get_relative_gen_dir(output)
1238                )
1239            ct = CustomTarget()
1240            ct.name = custom_target_.target_name_h()
1241            for src in relative_inputs_set:
1242                ct.srcs.append(src)
1243            for dep in depends:
1244                assert type(dep) is impl.CustomTarget
1245                ct.srcs.append(':' + dep.target_name())
1246            for out in relative_outputs:
1247                if _is_source(out):
1248                    out += obfuscate_suffix
1249                    # The scripts may still write to the assumed .c file, ensure the obfuscated
1250                    # file exists
1251                    command_line += (
1252                        "; echo '//nothing to see here' > " + _location_wrapper(out)
1253                    )
1254                ct.out.append(out)
1255            if python_script_target_name != '':
1256                ct.tools.append(python_script_target_name)
1257
1258            if program_command == 'bison' or program_command == 'flex':
1259                ct.tools.append('m4')
1260                ct.tools.append(program_command)
1261
1262            for dir in _get_export_include_dirs():
1263                ct.export_include_dirs.append(dir)
1264
1265            ct.cmd = command_line
1266            meson_translator.meson_state.custom_targets.append(ct)
1267            # Make a rule for only the sources
1268            obfuscate_suffix = '.dummy.c'
1269            wrapped_args = program_args + _get_command_args(
1270                command,
1271                input,
1272                output,
1273                depends,
1274                location_wrap=True,
1275                obfuscate_output_h=True,
1276                obfuscate_suffix=obfuscate_suffix,
1277            )
1278            if python_script:
1279                wrapped_args = _process_wrapped_args_for_python(
1280                    wrapped_args, python_script, python_script_target_name, depends
1281                )
1282
1283            command_line = impl.get_command_line_from_args(wrapped_args)
1284            if capture:
1285                command_line += ' > %s' % _location_wrapper(
1286                    impl.get_relative_gen_dir(output)
1287                )
1288
1289            # We keep the header as an output with an obfuscated name because some scripts insist
1290            # on having --out-h (like vk_entrypoints_gen.py). When Soong depends on this genrule
1291            # it'll insist on compiling all the outputs, so we replace the content of all header
1292            # outputs.
1293            ct_ = CustomTarget()
1294            ct_.name = custom_target_.target_name_c()
1295
1296            for src in relative_inputs_set:
1297                ct_.srcs.append(src)
1298            for dep in depends:
1299                assert type(dep) is impl.CustomTarget
1300                ct_.srcs.append(':' + dep.target_name())
1301            for out in relative_outputs:
1302                if _is_header(out):
1303                    out += obfuscate_suffix
1304                    ct_.out.append(out)
1305                    # Remove the content because Soong will compile this dummy source file
1306                    command_line += (
1307                        "; echo '//nothing to see here' > " + _location_wrapper(out)
1308                    )
1309                ct_.out.append(out)
1310            if python_script_target_name != '':
1311                ct_.tools.append(python_script_target_name)
1312            if program_command == 'bison' or program_command == 'flex':
1313                ct.tools.append('m4')
1314                ct.tools.append(program_command)
1315            ct_.cmd = command_line
1316            meson_translator.meson_state.custom_targets.append(ct_)
1317            return custom_target_
1318        else:
1319            wrapped_args = program_args + _get_command_args(
1320                command, input, output, depends, location_wrap=True
1321            )
1322            if python_script:
1323                wrapped_args = _process_wrapped_args_for_python(
1324                    wrapped_args, python_script, python_script_target_name, depends
1325                )
1326
1327            command_line = impl.get_command_line_from_args(wrapped_args)
1328            if capture:
1329                command_line += ' > %s' % _location_wrapper(
1330                    impl.get_relative_gen_dir(output)
1331                )
1332            ct = CustomTarget()
1333            ct.name = custom_target_.target_name()
1334            for src in relative_inputs_set:
1335                ct.srcs.append(src)
1336            for dep in depends:
1337                assert type(dep) is impl.CustomTarget
1338                ct.srcs.append(':' + dep.target_name())
1339            for out in relative_outputs:
1340                ct.out.append(out)
1341            if python_script_target_name != '':
1342                ct.tools.append(python_script_target_name)
1343            if program_command == 'bison' or program_command == 'flex':
1344                ct.tools.append('m4')
1345                ct.tools.append(program_command)
1346            for dir_ in _get_export_include_dirs():
1347                ct.export_include_dirs.append(dir_)
1348            ct.cmd = command_line
1349            meson_translator.meson_state.custom_targets.append(ct)
1350    else:  # is_bazel
1351        if program_command.endswith('.py'):
1352            program_command_arg = _location_wrapper(program_command)
1353        else:
1354            program_command_arg = program_command
1355
1356        program_args = [program_command_arg] + program_args
1357
1358        wrapped_args = program_args + _get_command_args(
1359            command, input, output, depends, location_wrap=True
1360        )
1361        if python_script:
1362            wrapped_args = _process_wrapped_args_for_python(
1363                wrapped_args, python_script, python_script_target_name, depends
1364            )
1365
1366        command_line = impl.get_command_line_from_args(wrapped_args)
1367        if capture:
1368            command_line += ' > %s' % _location_wrapper(
1369                impl.get_relative_gen_dir(output)
1370            )
1371
1372        ct = CustomTarget()
1373        ct.name = custom_target_.target_name()
1374        for src in set(relative_inputs):
1375            ct.srcs.append(src)
1376        for dep in depends:
1377            assert type(dep) is impl.CustomTarget
1378            ct.srcs.append(dep.target_name())
1379        for out in set(relative_outputs):
1380            ct.out.append(out)
1381        if python_script_target_name != '':
1382            ct.tools.append(python_script_target_name)
1383        ct.cmd = command_line
1384        meson_translator.meson_state.custom_targets.append(ct)
1385
1386    return custom_target_
1387