Skip to content
Snippets Groups Projects
Commit 2e8a820a authored by Adam J. Stewart's avatar Adam J. Stewart Committed by scheibelp
Browse files

Even better Makefile target parsing (#8819)

#8223 replaced regex-based makefile target parsing with an invocation of
"make -q". #8818 discovered that "make -q" can result in an error for some
packages.

Also, the "make -q" strategy relied on interpreting the error code, which only
worked for GNU Make and not BSD Make (which was deemed acceptable at
the time). As an added bonus, this implementation ignores the exit code and
instead parses STDERR for any indications that the target does not exist; this
works for both GNU Make and BSD Make.

#8223 also updated ninja target detection to use "ninja -t targets". This does
not change that behavior but makes it more-explicit with "ninja -t targets all"

This also adds tests for detection of "make" and "ninja" targets.
parent 2cd3e3fa
Branches
Tags
No related merge requests found
Showing
with 227 additions and 34 deletions
...@@ -132,6 +132,7 @@ addons: ...@@ -132,6 +132,7 @@ addons:
- graphviz - graphviz
- gnupg2 - gnupg2
- cmake - cmake
- ninja-build
- r-base - r-base
- r-base-core - r-base-core
- r-base-dev - r-base-dev
......
...@@ -1139,7 +1139,15 @@ def do_fake_install(self): ...@@ -1139,7 +1139,15 @@ def do_fake_install(self):
packages_dir = spack.store.layout.build_packages_path(self.spec) packages_dir = spack.store.layout.build_packages_path(self.spec)
dump_packages(self.spec, packages_dir) dump_packages(self.spec, packages_dir)
def _if_make_target_execute(self, target): def _has_make_target(self, target):
"""Checks to see if 'target' is a valid target in a Makefile.
Parameters:
target (str): the target to check for
Returns:
bool: True if 'target' is found, else False
"""
make = inspect.getmodule(self).make make = inspect.getmodule(self).make
# Check if we have a Makefile # Check if we have a Makefile
...@@ -1148,49 +1156,68 @@ def _if_make_target_execute(self, target): ...@@ -1148,49 +1156,68 @@ def _if_make_target_execute(self, target):
break break
else: else:
tty.msg('No Makefile found in the build directory') tty.msg('No Makefile found in the build directory')
return return False
# Check if 'target' is a valid target # Check if 'target' is a valid target.
#
# -q, --question
# ``Question mode''. Do not run any commands, or print anything;
# just return an exit status that is zero if the specified
# targets are already up to date, nonzero otherwise.
# #
# https://www.gnu.org/software/make/manual/html_node/Options-Summary.html # `make -n target` performs a "dry run". It prints the commands that
# would be run but doesn't actually run them. If the target does not
# exist, you will see one of the following error messages:
# #
# The exit status of make is always one of three values: # GNU Make:
# make: *** No rule to make target `test'. Stop.
# #
# 0 The exit status is zero if make is successful. # BSD Make:
# # make: don't know how to make test. Stop
# 2 The exit status is two if make encounters any errors. missing_target_msgs = [
# It will print messages describing the particular errors. "No rule to make target `{0}'. Stop.",
# "don't know how to make {0}. Stop",
# 1 The exit status is one if you use the '-q' flag and make ]
# determines that some target is not already up to date.
# kwargs = {
# https://www.gnu.org/software/make/manual/html_node/Running.html 'fail_on_error': False,
# 'output': os.devnull,
# NOTE: This only works for GNU Make, not NetBSD Make. 'error': str,
make('-q', target, fail_on_error=False) }
if make.returncode == 2:
tty.msg("Target '" + target + "' not found in " + makefile) stderr = make('-n', target, **kwargs)
return
for missing_target_msg in missing_target_msgs:
if missing_target_msg.format(target) in stderr:
tty.msg("Target '" + target + "' not found in " + makefile)
return False
# Execute target return True
make(target)
def _if_ninja_target_execute(self, target): def _if_make_target_execute(self, target):
"""Runs ``make target`` if 'target' is a valid target in the Makefile.
Parameters:
target (str): the target to potentially execute
"""
if self._has_make_target(target):
# Execute target
inspect.getmodule(self).make(target)
def _has_ninja_target(self, target):
"""Checks to see if 'target' is a valid target in a Ninja build script.
Parameters:
target (str): the target to check for
Returns:
bool: True if 'target' is found, else False
"""
ninja = inspect.getmodule(self).ninja ninja = inspect.getmodule(self).ninja
# Check if we have a Ninja build script # Check if we have a Ninja build script
if not os.path.exists('build.ninja'): if not os.path.exists('build.ninja'):
tty.msg('No Ninja build script found in the build directory') tty.msg('No Ninja build script found in the build directory')
return return False
# Get a list of all targets in the Ninja build script # Get a list of all targets in the Ninja build script
# https://ninja-build.org/manual.html#_extra_tools # https://ninja-build.org/manual.html#_extra_tools
all_targets = ninja('-t', 'targets', output=str).split('\n') all_targets = ninja('-t', 'targets', 'all', output=str).split('\n')
# Check if 'target' is a valid target # Check if 'target' is a valid target
matches = [line for line in all_targets matches = [line for line in all_targets
...@@ -1198,10 +1225,20 @@ def _if_ninja_target_execute(self, target): ...@@ -1198,10 +1225,20 @@ def _if_ninja_target_execute(self, target):
if not matches: if not matches:
tty.msg("Target '" + target + "' not found in build.ninja") tty.msg("Target '" + target + "' not found in build.ninja")
return return False
return True
def _if_ninja_target_execute(self, target):
"""Runs ``ninja target`` if 'target' is a valid target in the Ninja
build script.
# Execute target Parameters:
ninja(target) target (str): the target to potentially execute
"""
if self._has_ninja_target(target):
# Execute target
inspect.getmodule(self).ninja(target)
def _get_needed_resources(self): def _get_needed_resources(self):
resources = [] resources = []
......
...@@ -22,11 +22,101 @@ ...@@ -22,11 +22,101 @@
# License along with this program; if not, write to the Free Software # License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
############################################################################## ##############################################################################
import glob
import os
import pytest import pytest
import spack.repo import spack.repo
from spack.build_environment import get_std_cmake_args from llnl.util.filesystem import working_dir
from spack.build_environment import get_std_cmake_args, setup_package
from spack.spec import Spec from spack.spec import Spec
from spack.util.executable import which
DATA_PATH = os.path.join(spack.paths.test_path, 'data')
@pytest.mark.parametrize(
'directory',
glob.iglob(os.path.join(DATA_PATH, 'make', 'affirmative', '*'))
)
def test_affirmative_make_check(directory, config, mock_packages):
"""Tests that Spack correctly detects targets in a Makefile."""
# Get a fake package
s = Spec('mpich')
s.concretize()
pkg = spack.repo.get(s)
setup_package(pkg, False)
with working_dir(directory):
assert pkg._has_make_target('check')
pkg._if_make_target_execute('check')
@pytest.mark.parametrize(
'directory',
glob.iglob(os.path.join(DATA_PATH, 'make', 'negative', '*'))
)
def test_negative_make_check(directory, config, mock_packages):
"""Tests that Spack correctly ignores false positives in a Makefile."""
# Get a fake package
s = Spec('mpich')
s.concretize()
pkg = spack.repo.get(s)
setup_package(pkg, False)
with working_dir(directory):
assert not pkg._has_make_target('check')
pkg._if_make_target_execute('check')
@pytest.mark.skipif(not which('ninja'), reason='ninja is not installed')
@pytest.mark.parametrize(
'directory',
glob.iglob(os.path.join(DATA_PATH, 'ninja', 'affirmative', '*'))
)
def test_affirmative_ninja_check(directory, config, mock_packages):
"""Tests that Spack correctly detects targets in a Ninja build script."""
# Get a fake package
s = Spec('mpich')
s.concretize()
pkg = spack.repo.get(s)
setup_package(pkg, False)
with working_dir(directory):
assert pkg._has_ninja_target('check')
pkg._if_ninja_target_execute('check')
# Clean up Ninja files
for filename in glob.iglob('.ninja_*'):
os.remove(filename)
@pytest.mark.skipif(not which('ninja'), reason='ninja is not installed')
@pytest.mark.parametrize(
'directory',
glob.iglob(os.path.join(DATA_PATH, 'ninja', 'negative', '*'))
)
def test_negative_ninja_check(directory, config, mock_packages):
"""Tests that Spack correctly ignores false positives in a Ninja
build script."""
# Get a fake package
s = Spec('mpich')
s.concretize()
pkg = spack.repo.get(s)
setup_package(pkg, False)
with working_dir(directory):
assert not pkg._has_ninja_target('check')
pkg._if_ninja_target_execute('check')
def test_cmake_std_args(config, mock_packages): def test_cmake_std_args(config, mock_packages):
......
# Tests that Spack checks for Makefile
check:
# Tests that Spack detects target when it is the first of two targets
check test:
# Tests that Spack can handle variable expansion targets
TARGETS = check
$(TARGETS):
# Tests that Spack checks for GNUmakefile
check:
# Tests that Spack detects targets in include files
include make.mk
check:
# Tests that Spack checks for makefile
check:
# Tests that Spack detects a target even if it is followed by prerequisites
check: check-recursive
check-recursive:
# Tests that Spack allows spaces following the target name
check :
# Tests that Spack detects target when it is the second of two targets
test check:
# Tests that Spack detects a target if it is in the middle of a list
foo check bar:
# Tests that Spack ignores directories without a Makefile
check:
# Tests that Spack ignores targets that contain a partial match
checkinstall:
installcheck:
foo-check-bar:
foo_check_bar:
foo/check/bar:
# Tests that Spack ignores variable definitions
check = FOO
check := BAR
.ninja_deps
.ninja_log
# Tests that Spack detects target when it is the first of two targets
rule cc
command = true
build check test: cc
# Tests that Spack can handle targets in include files
include include.ninja
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment