Skip to content

Commit 4317edc

Browse files
authored
Merge pull request #1227 from centricular/ninja-clean-recursive-dir-delete
run_project_tests.py: Also do ninja clean on tests
2 parents 589a56e + a421976 commit 4317edc

File tree

6 files changed

+120
-36
lines changed

6 files changed

+120
-36
lines changed

mesonbuild/backend/backends.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
import subprocess
2323
from ..mesonlib import MesonException, get_compiler_for_source, classify_unity_sources
2424

25+
class CleanTrees():
26+
'''
27+
Directories outputted by custom targets that have to be manually cleaned
28+
because on Linux `ninja clean` only deletes empty directories.
29+
'''
30+
def __init__(self, build_dir, trees):
31+
self.build_dir = build_dir
32+
self.trees = trees
33+
2534
class InstallData():
2635
def __init__(self, source_dir, build_dir, prefix):
2736
self.source_dir = source_dir

mesonbuild/backend/ninjabackend.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from .. import dependencies
2121
from .. import compilers
2222
from ..mesonlib import File, MesonException, get_compiler_for_source, Popen_safe
23-
from .backends import InstallData
23+
from .backends import CleanTrees, InstallData
2424
from ..build import InvalidArguments
2525
import os, sys, pickle, re
2626
import subprocess, shutil
@@ -2109,6 +2109,22 @@ def generate_shlib_aliases(self, target, outdir):
21092109
except OSError:
21102110
mlog.debug("Library versioning disabled because we do not have symlink creation privileges.")
21112111

2112+
def generate_custom_target_clean(self, outfile, trees):
2113+
e = NinjaBuildElement(self.all_outputs, 'clean-ctlist', 'CUSTOM_COMMAND', 'PHONY')
2114+
d = CleanTrees(self.environment.get_build_dir(), trees)
2115+
d_file = os.path.join(self.environment.get_scratch_dir(), 'cleantrees.dat')
2116+
script_root = self.environment.get_script_dir()
2117+
clean_script = os.path.join(script_root, 'cleantrees.py')
2118+
e.add_item('COMMAND', [sys.executable,
2119+
self.environment.get_build_command(),
2120+
'--internal', 'cleantrees', d_file])
2121+
e.add_item('description', 'Cleaning CustomTarget directories')
2122+
e.write(outfile)
2123+
# Write out the data file passed to the script
2124+
with open(d_file, 'wb') as ofile:
2125+
pickle.dump(d, ofile)
2126+
return 'clean-ctlist'
2127+
21122128
def generate_gcov_clean(self, outfile):
21132129
gcno_elem = NinjaBuildElement(self.all_outputs, 'clean-gcno', 'CUSTOM_COMMAND', 'PHONY')
21142130
script_root = self.environment.get_script_dir()
@@ -2136,14 +2152,19 @@ def generate_utils(self, outfile):
21362152

21372153
def generate_ending(self, outfile):
21382154
targetlist = []
2155+
ctlist = []
21392156
for t in self.build.get_targets().values():
21402157
# RunTargets are meant to be invoked manually
21412158
if isinstance(t, build.RunTarget):
21422159
continue
2143-
# CustomTargets that aren't installed should only be built if they
2144-
# are used by something else or are meant to be always built
2145-
if isinstance(t, build.CustomTarget) and not (t.install or t.build_always):
2146-
continue
2160+
if isinstance(t, build.CustomTarget):
2161+
# Create a list of all custom target outputs
2162+
for o in t.get_outputs():
2163+
ctlist.append(os.path.join(self.get_target_dir(t), o))
2164+
# CustomTargets that aren't installed should only be built if
2165+
# they are used by something else or are to always be built
2166+
if not (t.install or t.build_always):
2167+
continue
21472168
# Add the first output of each target to the 'all' target so that
21482169
# they are all built
21492170
targetlist.append(os.path.join(self.get_target_dir(t), t.get_outputs()[0]))
@@ -2160,6 +2181,14 @@ def generate_ending(self, outfile):
21602181
elem = NinjaBuildElement(self.all_outputs, 'clean', 'CUSTOM_COMMAND', 'PHONY')
21612182
elem.add_item('COMMAND', [ninja_command, '-t', 'clean'])
21622183
elem.add_item('description', 'Cleaning')
2184+
# If we have custom targets in this project, add all their outputs to
2185+
# the list that is passed to the `cleantrees.py` script. The script
2186+
# will manually delete all custom_target outputs that are directories
2187+
# instead of files. This is needed because on platforms other than
2188+
# Windows, Ninja only deletes directories while cleaning if they are
2189+
# empty. https://github.com/mesonbuild/meson/issues/1220
2190+
if ctlist:
2191+
elem.add_dep(self.generate_custom_target_clean(outfile, ctlist))
21632192
if 'b_coverage' in self.environment.coredata.base_options and \
21642193
self.environment.coredata.base_options['b_coverage'].value:
21652194
self.generate_gcov_clean(outfile)

mesonbuild/coredata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ def get_builtin_option_default(optname):
236236
}
237237

238238
forbidden_target_names = {'clean': None,
239+
'clean-ctlist': None,
239240
'clean-gcno': None,
240241
'clean-gcda': None,
241242
'coverage-text': None,

mesonbuild/mesonmain.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ def run_script_command(args):
198198
if cmdname == 'exe':
199199
import mesonbuild.scripts.meson_exe as abc
200200
cmdfunc = abc.run
201+
elif cmdname == 'cleantrees':
202+
import mesonbuild.scripts.cleantrees as abc
203+
cmdfunc = abc.run
201204
elif cmdname == 'install':
202205
import mesonbuild.scripts.meson_install as abc
203206
cmdfunc = abc.run

mesonbuild/scripts/cleantrees.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2016 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+
15+
import os
16+
import sys
17+
import shutil
18+
import pickle
19+
20+
def rmtrees(build_dir, trees):
21+
for t in trees:
22+
# Never delete trees outside of the builddir
23+
if os.path.isabs(t):
24+
print('Cannot delete dir with absolute path {!r}'.format(t))
25+
continue
26+
bt = os.path.join(build_dir, t)
27+
# Skip if it doesn't exist, or if it is not a directory
28+
if os.path.isdir(bt):
29+
shutil.rmtree(bt, ignore_errors=True)
30+
31+
def run(args):
32+
if len(args) != 1:
33+
print('Cleaner script for Meson. Do not run on your own please.')
34+
print('cleantrees.py <data-file>')
35+
return 1
36+
with open(args[0], 'rb') as f:
37+
data = pickle.load(f)
38+
rmtrees(data.build_dir, data.trees)
39+
# Never fail cleaning
40+
return 0
41+
42+
if __name__ == '__main__':
43+
run(sys.argv[1:])

run_project_tests.py

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from mesonbuild import mesonlib
2525
from mesonbuild import mlog
2626
from mesonbuild import mesonmain
27-
from mesonbuild.mesonlib import stringlistify
27+
from mesonbuild.mesonlib import stringlistify, Popen_safe
2828
import argparse
2929
import xml.etree.ElementTree as ET
3030
import time
@@ -93,26 +93,20 @@ def stop_handler(signal, frame):
9393
backend_flags = None
9494
compile_commands = None
9595
test_commands = None
96-
install_commands = None
96+
install_commands = []
97+
clean_commands = []
9798

9899
def setup_commands(backend):
99-
global backend_flags, compile_commands, test_commands, install_commands
100+
global backend_flags, compile_commands, test_commands, install_commands, clean_commands
100101
msbuild_exe = shutil.which('msbuild')
101-
if backend == 'vs2010' or (backend is None and msbuild_exe is not None):
102-
backend_flags = ['--backend=vs2010']
102+
if (backend and backend.startswith('vs')) or (backend is None and msbuild_exe is not None):
103+
backend_flags = ['--backend=' + backend]
103104
compile_commands = ['msbuild']
104105
test_commands = ['msbuild', 'RUN_TESTS.vcxproj']
105-
install_commands = []
106-
elif backend == 'vs2015':
107-
backend_flags = ['--backend=vs2015']
108-
compile_commands = ['msbuild']
109-
test_commands = ['msbuild', 'RUN_TESTS.vcxproj']
110-
install_commands = []
111106
elif backend == 'xcode' or (backend is None and mesonlib.is_osx()):
112107
backend_flags = ['--backend=xcode']
113108
compile_commands = ['xcodebuild']
114109
test_commands = ['xcodebuild', '-target', 'RUN_TESTS']
115-
install_commands = []
116110
else:
117111
backend_flags = []
118112
ninja_command = environment.detect_ninja()
@@ -125,6 +119,7 @@ def setup_commands(backend):
125119
compile_commands += ['-w', 'dupbuild=err']
126120
test_commands = [ninja_command, 'test', 'benchmark']
127121
install_commands = [ninja_command, 'install']
122+
clean_commands = [ninja_command, 'clean']
128123

129124
def get_relative_files_list_from_dir(fromdir):
130125
paths = []
@@ -233,17 +228,18 @@ def parse_test_args(testdir):
233228
pass
234229
return args
235230

236-
def run_test(skipped, testdir, extra_args, flags, compile_commands, install_commands, should_fail):
231+
def run_test(skipped, testdir, extra_args, flags, compile_commands, should_fail):
237232
if skipped:
238233
return None
239234
with AutoDeletedDir(tempfile.mkdtemp(prefix='b ', dir='.')) as build_dir:
240235
with AutoDeletedDir(tempfile.mkdtemp(prefix='i ', dir=os.getcwd())) as install_dir:
241236
try:
242-
return _run_test(testdir, build_dir, install_dir, extra_args, flags, compile_commands, install_commands, should_fail)
237+
return _run_test(testdir, build_dir, install_dir, extra_args, flags, compile_commands, should_fail)
243238
finally:
244239
mlog.shutdown() # Close the log file because otherwise Windows wets itself.
245240

246-
def _run_test(testdir, test_build_dir, install_dir, extra_args, flags, compile_commands, install_commands, should_fail):
241+
def _run_test(testdir, test_build_dir, install_dir, extra_args, flags, compile_commands, should_fail):
242+
global install_commands, clean_commands
247243
test_args = parse_test_args(testdir)
248244
gen_start = time.time()
249245
gen_command = [meson_command, '--prefix', '/usr', '--libdir', 'lib', testdir, test_build_dir]\
@@ -268,12 +264,10 @@ def _run_test(testdir, test_build_dir, install_dir, extra_args, flags, compile_c
268264
else:
269265
comp = compile_commands
270266
build_start = time.time()
271-
pc = subprocess.Popen(comp, cwd=test_build_dir,
272-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
273-
(o, e) = pc.communicate()
267+
pc, o, e = Popen_safe(comp, cwd=test_build_dir)
274268
build_time = time.time() - build_start
275-
stdo += o.decode(sys.stdout.encoding)
276-
stde += e.decode(sys.stdout.encoding)
269+
stdo += o
270+
stde += e
277271
if should_fail == 'build':
278272
if pc.returncode != 0:
279273
return TestResult('', stdo, stde, mesonlog, gen_time)
@@ -294,19 +288,24 @@ def _run_test(testdir, test_build_dir, install_dir, extra_args, flags, compile_c
294288
return TestResult('Test that should have failed to run unit tests succeeded', stdo, stde, mesonlog, gen_time)
295289
if returncode != 0:
296290
return TestResult('Running unit tests failed.', stdo, stde, mesonlog, gen_time, build_time, test_time)
291+
# Do installation
297292
if len(install_commands) == 0:
298293
return TestResult('', '', '', gen_time, build_time, test_time)
299-
else:
294+
env = os.environ.copy()
295+
env['DESTDIR'] = install_dir
296+
pi, o, e = Popen_safe(install_commands, cwd=test_build_dir, env=env)
297+
stdo += o
298+
stde += e
299+
if pi.returncode != 0:
300+
return TestResult('Running install failed.', stdo, stde, mesonlog, gen_time, build_time, test_time)
301+
if len(clean_commands) != 0:
300302
env = os.environ.copy()
301-
env['DESTDIR'] = install_dir
302-
pi = subprocess.Popen(install_commands, cwd=test_build_dir, env=env,
303-
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
304-
(o, e) = pi.communicate()
305-
stdo += o.decode(sys.stdout.encoding)
306-
stde += e.decode(sys.stdout.encoding)
303+
pi, o, e = Popen_safe(clean_commands, cwd=test_build_dir, env=env)
304+
stdo += o
305+
stde += e
307306
if pi.returncode != 0:
308-
return TestResult('Running install failed.', stdo, stde, mesonlog, gen_time, build_time, test_time)
309-
return TestResult(validate_install(testdir, install_dir), stdo, stde, mesonlog, gen_time, build_time, test_time)
307+
return TestResult('Running clean failed.', stdo, stde, mesonlog, gen_time, build_time, test_time)
308+
return TestResult(validate_install(testdir, install_dir), stdo, stde, mesonlog, gen_time, build_time, test_time)
310309

311310
def gather_tests(testdir):
312311
tests = [t.replace('\\', '/').split('/', 2)[2] for t in glob(os.path.join(testdir, '*'))]
@@ -372,7 +371,7 @@ def detect_tests_to_run():
372371
return all_tests
373372

374373
def run_tests(extra_args):
375-
global passing_tests, failing_tests, stop, executor, futures
374+
global install_commands, passing_tests, failing_tests, stop, executor, futures
376375
all_tests = detect_tests_to_run()
377376
logfile = open('meson-test-run.txt', 'w', encoding="utf_8")
378377
junit_root = ET.Element('testsuites')
@@ -404,7 +403,7 @@ def run_tests(extra_args):
404403
should_fail = False
405404
if name.startswith('failing'):
406405
should_fail = name.split('failing-')[1]
407-
result = executor.submit(run_test, skipped, t, extra_args, unity_flags + backend_flags, compile_commands, install_commands, should_fail)
406+
result = executor.submit(run_test, skipped, t, extra_args, unity_flags + backend_flags, compile_commands, should_fail)
408407
futures.append((testname, t, result))
409408
for (testname, t, result) in futures:
410409
result = result.result()

0 commit comments

Comments
 (0)