1# Copyright 2016-2018 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 typing as T
16import time
17import sys, stat
18import datetime
19import os.path
20import platform
21import cProfile as profile
22import argparse
23import tempfile
24import shutil
25import glob
26
27from . import environment, interpreter, mesonlib
28from . import build
29from . import mlog, coredata
30from . import mintro
31from .mesonlib import MesonException
32
33git_ignore_file = '''# This file is autogenerated by Meson. If you change or delete it, it won't be recreated.
34*
35'''
36
37hg_ignore_file = '''# This file is autogenerated by Meson. If you change or delete it, it won't be recreated.
38syntax: glob
39**/*
40'''
41
42
43def add_arguments(parser: argparse.ArgumentParser) -> None:
44    coredata.register_builtin_arguments(parser)
45    parser.add_argument('--native-file',
46                        default=[],
47                        action='append',
48                        help='File containing overrides for native compilation environment.')
49    parser.add_argument('--cross-file',
50                        default=[],
51                        action='append',
52                        help='File describing cross compilation environment.')
53    parser.add_argument('--vsenv', action='store_true',
54                        help='Setup Visual Studio environment even when other compilers are found, ' +
55                             'abort if Visual Studio is not found. This option has no effect on other ' +
56                             'platforms than Windows. Defaults to True when using "vs" backend.')
57    parser.add_argument('-v', '--version', action='version',
58                        version=coredata.version)
59    parser.add_argument('--profile-self', action='store_true', dest='profile',
60                        help=argparse.SUPPRESS)
61    parser.add_argument('--fatal-meson-warnings', action='store_true', dest='fatal_warnings',
62                        help='Make all Meson warnings fatal')
63    parser.add_argument('--reconfigure', action='store_true',
64                        help='Set options and reconfigure the project. Useful when new ' +
65                             'options have been added to the project and the default value ' +
66                             'is not working.')
67    parser.add_argument('--wipe', action='store_true',
68                        help='Wipe build directory and reconfigure using previous command line options. ' +
69                             'Useful when build directory got corrupted, or when rebuilding with a ' +
70                             'newer version of meson.')
71    parser.add_argument('builddir', nargs='?', default=None)
72    parser.add_argument('sourcedir', nargs='?', default=None)
73
74class MesonApp:
75    def __init__(self, options: argparse.Namespace) -> None:
76        (self.source_dir, self.build_dir) = self.validate_dirs(options.builddir,
77                                                               options.sourcedir,
78                                                               options.reconfigure,
79                                                               options.wipe)
80        if options.wipe:
81            # Make a copy of the cmd line file to make sure we can always
82            # restore that file if anything bad happens. For example if
83            # configuration fails we need to be able to wipe again.
84            restore = []
85            with tempfile.TemporaryDirectory() as d:
86                for filename in [coredata.get_cmd_line_file(self.build_dir)] + glob.glob(os.path.join(self.build_dir, environment.Environment.private_dir, '*.ini')):
87                    try:
88                        restore.append((shutil.copy(filename, d), filename))
89                    except FileNotFoundError:
90                        raise MesonException(
91                            'Cannot find cmd_line.txt. This is probably because this '
92                            'build directory was configured with a meson version < 0.49.0.')
93
94                coredata.read_cmd_line_file(self.build_dir, options)
95
96                try:
97                    # Don't delete the whole tree, just all of the files and
98                    # folders in the tree. Otherwise calling wipe form the builddir
99                    # will cause a crash
100                    for l in os.listdir(self.build_dir):
101                        l = os.path.join(self.build_dir, l)
102                        if os.path.isdir(l) and not os.path.islink(l):
103                            mesonlib.windows_proof_rmtree(l)
104                        else:
105                            mesonlib.windows_proof_rm(l)
106                finally:
107                    self.add_vcs_ignore_files(self.build_dir)
108                    for b, f in restore:
109                        os.makedirs(os.path.dirname(f), exist_ok=True)
110                        shutil.move(b, f)
111
112        self.options = options
113
114    def has_build_file(self, dirname: str) -> bool:
115        fname = os.path.join(dirname, environment.build_filename)
116        return os.path.exists(fname)
117
118    def validate_core_dirs(self, dir1: str, dir2: str) -> T.Tuple[str, str]:
119        if dir1 is None:
120            if dir2 is None:
121                if not os.path.exists('meson.build') and os.path.exists('../meson.build'):
122                    dir2 = '..'
123                else:
124                    raise MesonException('Must specify at least one directory name.')
125            dir1 = os.getcwd()
126        if dir2 is None:
127            dir2 = os.getcwd()
128        ndir1 = os.path.abspath(os.path.realpath(dir1))
129        ndir2 = os.path.abspath(os.path.realpath(dir2))
130        if not os.path.exists(ndir1):
131            os.makedirs(ndir1)
132        if not os.path.exists(ndir2):
133            os.makedirs(ndir2)
134        if not stat.S_ISDIR(os.stat(ndir1).st_mode):
135            raise MesonException(f'{dir1} is not a directory')
136        if not stat.S_ISDIR(os.stat(ndir2).st_mode):
137            raise MesonException(f'{dir2} is not a directory')
138        if os.path.samefile(ndir1, ndir2):
139            # Fallback to textual compare if undefined entries found
140            has_undefined = any((s.st_ino == 0 and s.st_dev == 0) for s in (os.stat(ndir1), os.stat(ndir2)))
141            if not has_undefined or ndir1 == ndir2:
142                raise MesonException('Source and build directories must not be the same. Create a pristine build directory.')
143        if self.has_build_file(ndir1):
144            if self.has_build_file(ndir2):
145                raise MesonException(f'Both directories contain a build file {environment.build_filename}.')
146            return ndir1, ndir2
147        if self.has_build_file(ndir2):
148            return ndir2, ndir1
149        raise MesonException(f'Neither directory contains a build file {environment.build_filename}.')
150
151    def add_vcs_ignore_files(self, build_dir: str) -> None:
152        if os.listdir(build_dir):
153            return
154        with open(os.path.join(build_dir, '.gitignore'), 'w', encoding='utf-8') as ofile:
155            ofile.write(git_ignore_file)
156        with open(os.path.join(build_dir, '.hgignore'), 'w', encoding='utf-8') as ofile:
157            ofile.write(hg_ignore_file)
158
159    def validate_dirs(self, dir1: str, dir2: str, reconfigure: bool, wipe: bool) -> T.Tuple[str, str]:
160        (src_dir, build_dir) = self.validate_core_dirs(dir1, dir2)
161        self.add_vcs_ignore_files(build_dir)
162        priv_dir = os.path.join(build_dir, 'meson-private/coredata.dat')
163        if os.path.exists(priv_dir):
164            if not reconfigure and not wipe:
165                print('Directory already configured.\n'
166                      '\nJust run your build command (e.g. ninja) and Meson will regenerate as necessary.\n'
167                      'If ninja fails, run "ninja reconfigure" or "meson --reconfigure"\n'
168                      'to force Meson to regenerate.\n'
169                      '\nIf build failures persist, run "meson setup --wipe" to rebuild from scratch\n'
170                      'using the same options as passed when configuring the build.'
171                      '\nTo change option values, run "meson configure" instead.')
172                raise SystemExit
173        else:
174            has_cmd_line_file = os.path.exists(coredata.get_cmd_line_file(build_dir))
175            if (wipe and not has_cmd_line_file) or (not wipe and reconfigure):
176                raise SystemExit(f'Directory does not contain a valid build tree:\n{build_dir}')
177        return src_dir, build_dir
178
179    def generate(self) -> None:
180        env = environment.Environment(self.source_dir, self.build_dir, self.options)
181        mlog.initialize(env.get_log_dir(), self.options.fatal_warnings)
182        if self.options.profile:
183            mlog.set_timestamp_start(time.monotonic())
184        with mesonlib.BuildDirLock(self.build_dir):
185            self._generate(env)
186
187    def _generate(self, env: environment.Environment) -> None:
188        # Get all user defined options, including options that have been defined
189        # during a previous invocation or using meson configure.
190        user_defined_options = argparse.Namespace(**vars(self.options))
191        coredata.read_cmd_line_file(self.build_dir, user_defined_options)
192
193        mlog.debug('Build started at', datetime.datetime.now().isoformat())
194        mlog.debug('Main binary:', sys.executable)
195        mlog.debug('Build Options:', coredata.format_cmd_line_options(user_defined_options))
196        mlog.debug('Python system:', platform.system())
197        mlog.log(mlog.bold('The Meson build system'))
198        mlog.log('Version:', coredata.version)
199        mlog.log('Source dir:', mlog.bold(self.source_dir))
200        mlog.log('Build dir:', mlog.bold(self.build_dir))
201        if env.is_cross_build():
202            mlog.log('Build type:', mlog.bold('cross build'))
203        else:
204            mlog.log('Build type:', mlog.bold('native build'))
205        b = build.Build(env)
206
207        intr = interpreter.Interpreter(b, user_defined_options=user_defined_options)
208        if env.is_cross_build():
209            logger_fun = mlog.log
210        else:
211            logger_fun = mlog.debug
212        build_machine = intr.builtin['build_machine']
213        host_machine = intr.builtin['host_machine']
214        target_machine = intr.builtin['target_machine']
215        assert isinstance(build_machine, interpreter.MachineHolder)
216        assert isinstance(host_machine, interpreter.MachineHolder)
217        assert isinstance(target_machine, interpreter.MachineHolder)
218        logger_fun('Build machine cpu family:', mlog.bold(build_machine.cpu_family_method([], {})))
219        logger_fun('Build machine cpu:', mlog.bold(build_machine.cpu_method([], {})))
220        mlog.log('Host machine cpu family:', mlog.bold(host_machine.cpu_family_method([], {})))
221        mlog.log('Host machine cpu:', mlog.bold(host_machine.cpu_method([], {})))
222        logger_fun('Target machine cpu family:', mlog.bold(target_machine.cpu_family_method([], {})))
223        logger_fun('Target machine cpu:', mlog.bold(target_machine.cpu_method([], {})))
224        try:
225            if self.options.profile:
226                fname = os.path.join(self.build_dir, 'meson-private', 'profile-interpreter.log')
227                profile.runctx('intr.run()', globals(), locals(), filename=fname)
228            else:
229                intr.run()
230        except Exception as e:
231            mintro.write_meson_info_file(b, [e])
232            raise
233        try:
234            dumpfile = os.path.join(env.get_scratch_dir(), 'build.dat')
235            # We would like to write coredata as late as possible since we use the existence of
236            # this file to check if we generated the build file successfully. Since coredata
237            # includes settings, the build files must depend on it and appear newer. However, due
238            # to various kernel caches, we cannot guarantee that any time in Python is exactly in
239            # sync with the time that gets applied to any files. Thus, we dump this file as late as
240            # possible, but before build files, and if any error occurs, delete it.
241            cdf = env.dump_coredata()
242            if self.options.profile:
243                fname = f'profile-{intr.backend.name}-backend.log'
244                fname = os.path.join(self.build_dir, 'meson-private', fname)
245                profile.runctx('intr.backend.generate()', globals(), locals(), filename=fname)
246            else:
247                intr.backend.generate()
248            b.devenv.append(intr.backend.get_devenv())
249            build.save(b, dumpfile)
250            if env.first_invocation:
251                # Use path resolved by coredata because they could have been
252                # read from a pipe and wrote into a private file.
253                self.options.cross_file = env.coredata.cross_files
254                self.options.native_file = env.coredata.config_files
255                coredata.write_cmd_line_file(self.build_dir, self.options)
256            else:
257                coredata.update_cmd_line_file(self.build_dir, self.options)
258
259            # Generate an IDE introspection file with the same syntax as the already existing API
260            if self.options.profile:
261                fname = os.path.join(self.build_dir, 'meson-private', 'profile-introspector.log')
262                profile.runctx('mintro.generate_introspection_file(b, intr.backend)', globals(), locals(), filename=fname)
263            else:
264                mintro.generate_introspection_file(b, intr.backend)
265            mintro.write_meson_info_file(b, [], True)
266
267            # Post-conf scripts must be run after writing coredata or else introspection fails.
268            intr.backend.run_postconf_scripts()
269
270            # collect warnings about unsupported build configurations; must be done after full arg processing
271            # by Interpreter() init, but this is most visible at the end
272            if env.coredata.options[mesonlib.OptionKey('backend')].value == 'xcode':
273                mlog.warning('xcode backend is currently unmaintained, patches welcome')
274            if env.coredata.options[mesonlib.OptionKey('layout')].value == 'flat':
275                mlog.warning('-Dlayout=flat is unsupported and probably broken. It was a failed experiment at '
276                             'making Windows build artifacts runnable while uninstalled, due to PATH considerations, '
277                             'but was untested by CI and anyways breaks reasonable use of conflicting targets in different subdirs. '
278                             'Please consider using `meson devenv` instead. See https://github.com/mesonbuild/meson/pull/9243 '
279                             'for details.')
280
281        except Exception as e:
282            mintro.write_meson_info_file(b, [e])
283            if 'cdf' in locals():
284                old_cdf = cdf + '.prev'
285                if os.path.exists(old_cdf):
286                    os.replace(old_cdf, cdf)
287                else:
288                    os.unlink(cdf)
289            raise
290
291def run(options: argparse.Namespace) -> int:
292    coredata.parse_cmd_line_options(options)
293    app = MesonApp(options)
294    app.generate()
295    return 0
296