Skip to content

Commit

Permalink
feat: add cmake_target to change the target used by CMake (scikit-bui…
Browse files Browse the repository at this point in the history
…ld#477)

Parses the command line for the `--target` option.
And also provide the `cmake_install_target` parameter in `setup.py`

The command line argument has preference over the `cmake_install_target` from
`setup.py`.

This allows the user to point to targets other than 'install'.
The user has to set a target in CMake to install only a specific
component.

```cmake
install(TARGETS foo COMPONENT runtime)
add_custom_target(foo-install-runtime
    ${CMAKE_COMMAND}
    -DCMAKE_INSTALL_COMPONENT=runtime
    -P "${PROJECT_BINARY_DIR}/cmake_install.cmake")
```

DevNote: There is a refactor of the function `cmaker.make`
That allows calling the function twice in case the target is
not the default `install`.
  • Loading branch information
phcerdan authored Feb 8, 2022
1 parent 22b96b7 commit b76d0d1
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ Scikit-build 0.11.0

New Features
------------
* Add support for `--install-target` scikit-build command line option.
And `cmake_install_target` in `setup.py`. Allowing to
provide an install target different than the default `install`.
Thanks :user:`phcerdan` for the contribution. See :issue:`477`.

* Add a hook to process the cmake install manifest building the wheel. The hook
function can be specified as an argument to the ``setup()`` function. This can be used e.g.
Expand Down
15 changes: 15 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,21 @@ For example::
- ``cmake_minimum_required_version``: String identifying the minimum version of CMake required
to configure the project.

- ``cmake_install_target``: Name of the target to "build" for installing the artifacts into the wheel.
By default, this option is set to ``install``, which is always provided by CMake.
This can be used to only install certain components.

For example::

install(TARGETS foo COMPONENT runtime)
add_custom_target(foo-install-runtime
${CMAKE_COMMAND}
-DCMAKE_INSTALL_COMPONENT=runtime
-P "${PROJECT_BINARY_DIR}/cmake_install.cmake"
DEPENDS foo
)


Scikit-build changes the following options:

.. versionadded:: 0.7.0
Expand Down
56 changes: 52 additions & 4 deletions skbuild/cmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,35 +587,83 @@ def check_for_bad_installs():
(" " + _install) for _install in bad_installs)
)))

def make(self, clargs=(), config="Release", source_dir=".", env=None):
def make(self, clargs=(), config="Release", source_dir=".",
install_target="install", env=None):
"""Calls the system-specific make program to compile code.
install_target: string
Name of the target responsible to install the project.
Default is "install".
.. note::
To workaround CMake issue #8438.
See https://gitlab.kitware.com/cmake/cmake/-/issues/8438
Due to a limitation of CMake preventing from adding a dependency
on the "build-all" built-in target, we explicitly build the project first when
the install target is different from the default on.
"""
clargs, config = pop_arg('--config', clargs, config)
clargs, install_target = pop_arg('--install-target', clargs, install_target)
if not os.path.exists(CMAKE_BUILD_DIR()):
raise SKBuildError(("CMake build folder ({}) does not exist. "
"Did you forget to run configure before "
"make?").format(CMAKE_BUILD_DIR()))

cmd = [self.cmake_executable, "--build", source_dir,
"--target", "install", "--config", config, "--"]
# Workaround CMake issue #8438
# See https://gitlab.kitware.com/cmake/cmake/-/issues/8438
# Due to a limitation of CMake preventing from adding a dependency
# on the "build-all" built-in target, we explicitly build
# the project first when
# the install target is different from the default on.
if install_target != "install":
self.make_impl(clargs=clargs, config=config, source_dir=source_dir,
install_target=None, env=env)

self.make_impl(clargs=clargs, config=config, source_dir=source_dir,
install_target=install_target, env=env)

def make_impl(self, clargs, config, source_dir, install_target, env=None):
"""
Precondition: clargs does not have --config nor --install-target options.
These command line arguments are extracted in the caller function
`make` with `clargs, config = pop_arg('--config', clargs, config)`
This is a refactor effort for calling the function `make` twice in
case the install_target is different than the default `install`.
"""
if not install_target:
cmd = [self.cmake_executable, "--build", source_dir,
"--config", config, "--"]
else:
cmd = [self.cmake_executable, "--build", source_dir,
"--target", install_target, "--config", config, "--"]
cmd.extend(clargs)
cmd.extend(
filter(bool,
shlex.split(os.environ.get("SKBUILD_BUILD_OPTIONS", "")))
)

rtn = subprocess.call(cmd, cwd=CMAKE_BUILD_DIR(), env=env)
# For reporting errors (if any)
if not install_target:
install_target = "internal build step [valid]"

if rtn != 0:
raise SKBuildError(
"An error occurred while building with CMake.\n"
" Command:\n"
" {}\n"
" Install target:\n"
" {}\n"
" Source directory:\n"
" {}\n"
" Working directory:\n"
" {}\n"
"Please see CMake's output for more information.".format(
"Please check the install target is valid and see CMake's output for more "
"information.".format(
self._formatArgsForDisplay(cmd),
install_target,
os.path.abspath(source_dir),
os.path.abspath(CMAKE_BUILD_DIR())))

Expand Down
26 changes: 24 additions & 2 deletions skbuild/setuptools_wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ def create_skbuild_argparser():
'--cmake-executable', default=None, metavar='',
help='specify the path to the cmake executable'
)
parser.add_argument(
'--install-target', default=None, metavar='',
help='specify the CMake target performing the install. '
'If not provided, uses the target ``install``'
)
parser.add_argument(
'--skip-generator-test', action='store_true',
help='skip generator test when a generator is explicitly selected using --generator'
Expand Down Expand Up @@ -112,6 +117,8 @@ def parse_skbuild_args(args, cmake_args, build_tool_args):
build_tool_args.extend(['--config', namespace.build_type])
if namespace.jobs is not None:
build_tool_args.extend(['-j', str(namespace.jobs)])
if namespace.install_target is not None:
build_tool_args.extend(['--install-target', namespace.install_target])

if namespace.generator is None and namespace.skip_generator_test is True:
sys.exit("ERROR: Specifying --skip-generator-test requires --generator to also be specified.")
Expand Down Expand Up @@ -401,7 +408,8 @@ def setup(*args, **kw): # noqa: C901
'cmake_with_sdist': False,
'cmake_languages': ('C', 'CXX'),
'cmake_minimum_required_version': None,
'cmake_process_manifest_hook': None
'cmake_process_manifest_hook': None,
'cmake_install_target': 'install'
}
skbuild_kw = {param: kw.pop(param, parameters[param])
for param in parameters}
Expand Down Expand Up @@ -492,6 +500,20 @@ def setup(*args, **kw): # noqa: C901
# one is considered, let's prepend the one provided in the setup call.
cmake_args = skbuild_kw['cmake_args'] + cmake_args

# Handle cmake_install_target
# get the target (next item after '--install-target') or return '' if no --install-target
cmake_install_target_from_command = next(
(make_args[index+1] for index, item in enumerate(make_args) if
item == '--install-target'), '')
cmake_install_target_from_setup = skbuild_kw['cmake_install_target']
# Setting target from command takes precedence
# cmake_install_target_from_setup has the default 'install',
# so cmake_install_target would never be empty.
if cmake_install_target_from_command:
cmake_install_target = cmake_install_target_from_command
else:
cmake_install_target = cmake_install_target_from_setup

if sys.platform == 'darwin':

# If no ``--plat-name`` argument was passed, set default value.
Expand Down Expand Up @@ -592,7 +614,7 @@ def setup(*args, **kw): # noqa: C901
languages=cmake_languages
)
_save_cmake_spec(cmake_spec)
cmkr.make(make_args, env=env)
cmkr.make(make_args, install_target=cmake_install_target, env=env)
except SKBuildGeneratorNotFoundError as ex:
sys.exit(ex)
except SKBuildError as ex:
Expand Down
14 changes: 14 additions & 0 deletions tests/samples/test-cmake-target/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.5.0)
project(test-cmake-target NONE)
file(WRITE "${CMAKE_BINARY_DIR}/foo.txt" "# foo")
file(WRITE "${CMAKE_BINARY_DIR}/runtime.txt" "# runtime")
install(FILES "${CMAKE_BINARY_DIR}/foo.txt" DESTINATION ".")
install(CODE "message(STATUS \"Project has been installed\")")
install(FILES "${CMAKE_BINARY_DIR}/runtime.txt" DESTINATION "." COMPONENT runtime)
install(CODE "message(STATUS \"Runtime component has been installed\")" COMPONENT runtime)
# Add custom target to only install component: runtime (libraries)
add_custom_target(install-runtime
${CMAKE_COMMAND}
-DCMAKE_INSTALL_COMPONENT=runtime
-P "${PROJECT_BINARY_DIR}/cmake_install.cmake"
)
10 changes: 10 additions & 0 deletions tests/samples/test-cmake-target/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from skbuild import setup

setup(
name="test-cmake-target",
version="1.2.3",
description="a minimal example package using a non-default target",
author='The scikit-build team',
license="MIT",
cmake_target="install-runtime",
)
19 changes: 19 additions & 0 deletions tests/test_cmake_target.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""test_cmake_target
----------------------------------
Tries to build and test the `test-cmake-target` sample
project. It basically checks that using the `cmake_target` keyword
in setup.py works.
"""

from . import project_setup_py_test


@project_setup_py_test("test-cmake-target", ["build"], disable_languages_test=True)
def test_cmake_target_build(capsys):
out, err = capsys.readouterr()
dist_warning = "Unknown distribution option: 'cmake_target'"
assert (dist_warning not in err and dist_warning not in out)
49 changes: 49 additions & 0 deletions tests/test_cmaker.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,55 @@ def test_make(configure_with_cmake_source_dir, capfd):
assert message in out


@pytest.mark.parametrize("install_target", ("", "install", "install-runtime", "nonexistant-install-target"))
def test_make_with_install_target(install_target, capfd):
tmp_dir = _tmpdir('test_make_with_install_target')
with push_dir(str(tmp_dir)):

tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
"""
cmake_minimum_required(VERSION 3.5.0)
project(foobar NONE)
file(WRITE "${CMAKE_BINARY_DIR}/foo.txt" "# foo")
file(WRITE "${CMAKE_BINARY_DIR}/runtime.txt" "# runtime")
install(FILES "${CMAKE_BINARY_DIR}/foo.txt" DESTINATION ".")
install(CODE "message(STATUS \\"Project has been installed\\")")
install(FILES "${CMAKE_BINARY_DIR}/runtime.txt" DESTINATION "." COMPONENT runtime)
install(CODE "message(STATUS \\"Runtime component has been installed\\")" COMPONENT runtime)
# Add custom target to only install component: runtime (libraries)
add_custom_target(install-runtime
${CMAKE_COMMAND}
-DCMAKE_INSTALL_COMPONENT=runtime
-P "${PROJECT_BINARY_DIR}/cmake_install.cmake"
)
"""
))

with push_dir(str(tmp_dir)):
cmkr = CMaker()
env = cmkr.configure()
if install_target in ["", "install", "install-runtime"]:
cmkr.make(install_target=install_target, env=env)
else:
with pytest.raises(SKBuildError) as excinfo:
cmkr.make(install_target=install_target, env=env)
assert "check the install target is valid" in str(excinfo.value)

out, err = capfd.readouterr()
# This message appears with both install_targets: default 'install' and
# 'install-runtime'
message = "Runtime component has been installed"
if install_target in ["install", "install-runtime"]:
assert message in out

# One of these error appears with install_target: nonexistant-install-target
err_message1 = "No rule to make target"
err_message2 = "unknown target"
if install_target == "nonexistant-install-target":
assert err_message1 in err or err_message2 in err


def test_configure_with_cmake_args(capfd):
tmp_dir = _tmpdir('test_configure_with_cmake_args')
with push_dir(str(tmp_dir)):
Expand Down

0 comments on commit b76d0d1

Please sign in to comment.