1#!/usr/bin/env python3
2
3# Copyright 2018 The Meson development team
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import os
18import tempfile
19import unittest
20import subprocess
21import zipapp
22from pathlib import Path
23
24from mesonbuild.mesonlib import windows_proof_rmtree, python_command, is_windows
25from mesonbuild.coredata import version as meson_version
26
27
28def get_pypath():
29    import sysconfig
30    pypath = sysconfig.get_path('purelib', vars={'base': ''})
31    # Ensure that / is the path separator and not \, then strip /
32    return Path(pypath).as_posix().strip('/')
33
34def get_pybindir():
35    import sysconfig
36    # 'Scripts' on Windows and 'bin' on other platforms including MSYS
37    return sysconfig.get_path('scripts', vars={'base': ''}).strip('\\/')
38
39class CommandTests(unittest.TestCase):
40    '''
41    Test that running meson in various ways works as expected by checking the
42    value of mesonlib.meson_command that was set during configuration.
43    '''
44
45    def setUp(self):
46        super().setUp()
47        self.orig_env = os.environ.copy()
48        self.orig_dir = os.getcwd()
49        os.environ['MESON_COMMAND_TESTS'] = '1'
50        self.tmpdir = Path(tempfile.mkdtemp()).resolve()
51        self.src_root = Path(__file__).resolve().parent
52        self.testdir = str(self.src_root / 'test cases/common/1 trivial')
53        self.meson_args = ['--backend=ninja']
54
55    def tearDown(self):
56        try:
57            windows_proof_rmtree(str(self.tmpdir))
58        except FileNotFoundError:
59            pass
60        os.environ.clear()
61        os.environ.update(self.orig_env)
62        os.chdir(str(self.orig_dir))
63        super().tearDown()
64
65    def _run(self, command, workdir=None):
66        '''
67        Run a command while printing the stdout, and also return a copy of it
68        '''
69        # If this call hangs CI will just abort. It is very hard to distinguish
70        # between CI issue and test bug in that case. Set timeout and fail loud
71        # instead.
72        p = subprocess.run(command, stdout=subprocess.PIPE,
73                           env=os.environ.copy(), universal_newlines=True,
74                           cwd=workdir, timeout=60 * 5)
75        print(p.stdout)
76        if p.returncode != 0:
77            raise subprocess.CalledProcessError(p.returncode, command)
78        return p.stdout
79
80    def assertMesonCommandIs(self, line, cmd):
81        self.assertTrue(line.startswith('meson_command '), msg=line)
82        self.assertEqual(line, f'meson_command is {cmd!r}')
83
84    def test_meson_uninstalled(self):
85        # This is what the meson command must be for all these cases
86        resolved_meson_command = python_command + [str(self.src_root / 'meson.py')]
87        # Absolute path to meson.py
88        os.chdir('/')
89        builddir = str(self.tmpdir / 'build1')
90        meson_py = str(self.src_root / 'meson.py')
91        meson_setup = [meson_py, 'setup']
92        meson_command = python_command + meson_setup + self.meson_args
93        stdo = self._run(meson_command + [self.testdir, builddir])
94        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
95        # ./meson.py
96        os.chdir(str(self.src_root))
97        builddir = str(self.tmpdir / 'build2')
98        meson_py = './meson.py'
99        meson_setup = [meson_py, 'setup']
100        meson_command = python_command + meson_setup + self.meson_args
101        stdo = self._run(meson_command + [self.testdir, builddir])
102        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
103        # Symlink to meson.py
104        if is_windows():
105            # Symlinks require admin perms
106            return
107        os.chdir(str(self.src_root))
108        builddir = str(self.tmpdir / 'build3')
109        # Create a symlink to meson.py in bindir, and add it to PATH
110        bindir = (self.tmpdir / 'bin')
111        bindir.mkdir()
112        (bindir / 'meson').symlink_to(self.src_root / 'meson.py')
113        os.environ['PATH'] = str(bindir) + os.pathsep + os.environ['PATH']
114        # See if it works!
115        meson_py = 'meson'
116        meson_setup = [meson_py, 'setup']
117        meson_command = meson_setup + self.meson_args
118        stdo = self._run(meson_command + [self.testdir, builddir])
119        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
120
121    def test_meson_installed(self):
122        # Install meson
123        prefix = self.tmpdir / 'prefix'
124        pylibdir = prefix / get_pypath()
125        bindir = prefix / get_pybindir()
126        pylibdir.mkdir(parents=True)
127        # XXX: join with empty name so it always ends with os.sep otherwise
128        # distutils complains that prefix isn't contained in PYTHONPATH
129        os.environ['PYTHONPATH'] = os.path.join(str(pylibdir), '')
130        os.environ['PATH'] = str(bindir) + os.pathsep + os.environ['PATH']
131        self._run(python_command + ['setup.py', 'install', '--prefix', str(prefix)])
132        # Fix importlib-metadata by appending all dirs in pylibdir
133        PYTHONPATHS = [pylibdir] + [x for x in pylibdir.iterdir()]
134        PYTHONPATHS = [os.path.join(str(x), '') for x in PYTHONPATHS]
135        os.environ['PYTHONPATH'] = os.pathsep.join(PYTHONPATHS)
136        # Check that all the files were installed correctly
137        self.assertTrue(bindir.is_dir())
138        self.assertTrue(pylibdir.is_dir())
139        # Run `meson`
140        os.chdir('/')
141        resolved_meson_command = [str(bindir / 'meson')]
142        builddir = str(self.tmpdir / 'build1')
143        meson_setup = ['meson', 'setup']
144        meson_command = meson_setup + self.meson_args
145        stdo = self._run(meson_command + [self.testdir, builddir])
146        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
147        # Run `/path/to/meson`
148        builddir = str(self.tmpdir / 'build2')
149        meson_setup = [str(bindir / 'meson'), 'setup']
150        meson_command = meson_setup + self.meson_args
151        stdo = self._run(meson_command + [self.testdir, builddir])
152        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
153        # Run `python3 -m mesonbuild.mesonmain`
154        resolved_meson_command = python_command + ['-m', 'mesonbuild.mesonmain']
155        builddir = str(self.tmpdir / 'build3')
156        meson_setup = ['-m', 'mesonbuild.mesonmain', 'setup']
157        meson_command = python_command + meson_setup + self.meson_args
158        stdo = self._run(meson_command + [self.testdir, builddir])
159        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
160        if is_windows():
161            # Next part requires a shell
162            return
163        # `meson` is a wrapper to `meson.real`
164        resolved_meson_command = [str(bindir / 'meson.real')]
165        builddir = str(self.tmpdir / 'build4')
166        (bindir / 'meson').rename(bindir / 'meson.real')
167        wrapper = (bindir / 'meson')
168        wrapper.write_text('#!/bin/sh\n\nmeson.real "$@"', encoding='utf-8')
169        wrapper.chmod(0o755)
170        meson_setup = [str(wrapper), 'setup']
171        meson_command = meson_setup + self.meson_args
172        stdo = self._run(meson_command + [self.testdir, builddir])
173        self.assertMesonCommandIs(stdo.split('\n')[0], resolved_meson_command)
174
175    def test_meson_exe_windows(self):
176        raise unittest.SkipTest('NOT IMPLEMENTED')
177
178    def test_meson_zipapp(self):
179        if is_windows():
180            raise unittest.SkipTest('NOT IMPLEMENTED')
181        source = Path(__file__).resolve().parent
182        target = self.tmpdir / 'meson.pyz'
183        script = source / 'packaging' / 'create_zipapp.py'
184        self._run([script.as_posix(), source, '--outfile', target, '--interpreter', python_command[0]])
185        self._run([target.as_posix(), '--help'])
186
187
188if __name__ == '__main__':
189    print('Meson build system', meson_version, 'Command Tests')
190    raise SystemExit(unittest.main(buffer=True))
191