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