1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""test_setup
5----------------------------------
6
7Tests for `skbuild.setup` function.
8"""
9
10import textwrap
11import sys
12import os
13import pprint
14import py.path
15import pytest
16
17from distutils.core import Distribution as distutils_Distribution
18from mock import patch
19from setuptools import Distribution as setuptool_Distribution
20
21from skbuild import setup as skbuild_setup
22from skbuild.constants import CMAKE_INSTALL_DIR, SKBUILD_DIR
23from skbuild.exceptions import SKBuildError
24from skbuild.platform_specifics import get_platform
25from skbuild.setuptools_wrap import strip_package
26from skbuild.utils import (push_dir, to_platform_path)
27
28from . import (_tmpdir, execute_setup_py,
29               initialize_git_repo_and_commit, is_site_reachable, push_argv)
30
31
32@pytest.mark.parametrize("distribution_type",
33                         ['unknown',
34                          'py_modules',
35                          'packages',
36                          'skbuild'
37                          ])
38def test_distribution_is_pure(distribution_type, tmpdir):
39
40    skbuild_setup_kwargs = {}
41
42    if distribution_type == 'unknown':
43        is_pure = False
44
45    elif distribution_type == 'py_modules':
46        is_pure = True
47        hello_py = tmpdir.join("hello.py")
48        hello_py.write("")
49        skbuild_setup_kwargs["py_modules"] = ["hello"]
50
51    elif distribution_type == 'packages':
52        is_pure = True
53        init_py = tmpdir.mkdir("hello").join("__init__.py")
54        init_py.write("")
55        skbuild_setup_kwargs["packages"] = ["hello"]
56
57    elif distribution_type == 'skbuild':
58        is_pure = False
59        cmakelists_txt = tmpdir.join("CMakeLists.txt")
60        cmakelists_txt.write(
61            """
62            cmake_minimum_required(VERSION 3.5.0)
63            project(test NONE)
64            install(CODE "execute_process(
65              COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
66            """
67        )
68    else:
69        raise Exception(
70            "Unknown distribution_type: {}".format(distribution_type))
71
72    platform = get_platform()
73    original_write_test_cmakelist = platform.write_test_cmakelist
74
75    def write_test_cmakelist_no_languages(_self, _languages):
76        original_write_test_cmakelist([])
77
78    with patch.object(type(platform), 'write_test_cmakelist', new=write_test_cmakelist_no_languages):
79
80        with push_dir(str(tmpdir)), push_argv(["setup.py", "build"]):
81            distribution = skbuild_setup(
82                name="test",
83                version="0.0.1",
84                description="test object returned by setup function",
85                author="The scikit-build team",
86                license="MIT",
87                **skbuild_setup_kwargs
88            )
89            assert issubclass(distribution.__class__,
90                              (distutils_Distribution, setuptool_Distribution))
91            assert is_pure == distribution.is_pure()
92
93
94@pytest.mark.parametrize("cmake_args", [
95    [],
96    ['--', '-DVAR:STRING=43', '-DVAR_WITH_SPACE:STRING=Ciao Mondo']
97])
98def test_cmake_args_keyword(cmake_args, capfd):
99    tmp_dir = _tmpdir('cmake_args_keyword')
100
101    tmp_dir.join('setup.py').write(textwrap.dedent(
102        """
103        from skbuild import setup
104        setup(
105            name="test_cmake_args_keyword",
106            version="1.2.3",
107            description="a minimal example package",
108            author='The scikit-build team',
109            license="MIT",
110            cmake_args=[
111                "-DVAR:STRING=42",
112                "-DVAR_WITH_SPACE:STRING=Hello World"
113            ]
114
115        )
116        """
117    ))
118    tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
119        """
120        cmake_minimum_required(VERSION 3.5.0)
121        project(test NONE)
122        message(STATUS "VAR[${VAR}]")
123        message(STATUS "VAR_WITH_SPACE[${VAR_WITH_SPACE}]")
124        install(CODE "execute_process(
125          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
126        """
127    ))
128
129    with execute_setup_py(tmp_dir, ['build'] + cmake_args, disable_languages_test=True):
130        pass
131
132    out, _ = capfd.readouterr()
133
134    if not cmake_args:
135        assert "VAR[42]" in out
136        assert "VAR_WITH_SPACE[Hello World]" in out
137    else:
138        assert "VAR[43]" in out
139        assert "VAR_WITH_SPACE[Ciao Mondo]" in out
140
141
142@pytest.mark.parametrize(
143    "cmake_install_dir, expected_failed, error_code_type", (
144        (None, True, str),
145        ('', True, str),
146        (str(py.path.local.get_temproot().join('scikit-build')), True, SKBuildError),
147        ('banana', False, str)
148    )
149)
150def test_cmake_install_dir_keyword(
151        cmake_install_dir, expected_failed, error_code_type, capsys):
152
153    # -------------------------------------------------------------------------
154    # "SOURCE" tree layout:
155    #
156    # ROOT/
157    #
158    #     CMakeLists.txt
159    #     setup.py
160    #
161    #     apple/
162    #         __init__.py
163    #
164    # -------------------------------------------------------------------------
165    # "BINARY" distribution layout
166    #
167    # ROOT/
168    #
169    #     apple/
170    #         __init__.py
171    #
172
173    tmp_dir = _tmpdir('cmake_install_dir_keyword')
174
175    setup_kwarg = ''
176    if cmake_install_dir is not None:
177        setup_kwarg = 'cmake_install_dir=r\'{}\''.format(cmake_install_dir)
178
179    tmp_dir.join('setup.py').write(textwrap.dedent(
180        """
181        from skbuild import setup
182        setup(
183            name="test_cmake_install_dir",
184            version="1.2.3",
185            description="a package testing use of cmake_install_dir",
186            author='The scikit-build team',
187            license="MIT",
188            packages=['apple', 'banana'],
189            {setup_kwarg}
190        )
191        """.format(setup_kwarg=setup_kwarg)
192    ))
193
194    # Install location purposely set to "." so that we can test
195    # usage of "cmake_install_dir" skbuild.setup keyword.
196    tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
197        """
198        cmake_minimum_required(VERSION 3.5.0)
199        project(banana NONE)
200        file(WRITE "${CMAKE_BINARY_DIR}/__init__.py" "")
201        install(FILES "${CMAKE_BINARY_DIR}/__init__.py" DESTINATION ".")
202        """
203    ))
204
205    tmp_dir.ensure('apple', '__init__.py')
206
207    failed = False
208    message = ""
209    try:
210        with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
211            pass
212    except SystemExit as e:
213        # Error is not of type SKBuildError, it is expected to be
214        # raised by distutils.core.setup
215        failed = isinstance(e.code, error_code_type)
216        message = str(e)
217
218    out, _ = capsys.readouterr()
219
220    assert failed == expected_failed
221    if failed:
222        if error_code_type == str:
223            assert message == "error: package directory " \
224                              "'{}' does not exist".format(
225                                    os.path.join(CMAKE_INSTALL_DIR(), 'banana'))
226        else:
227            assert message.strip().startswith(
228                "setup parameter 'cmake_install_dir' "
229                "is set to an absolute path.")
230    else:
231        init_py = to_platform_path("{}/banana/__init__.py".format(CMAKE_INSTALL_DIR()))
232        assert "copying {}".format(init_py) in out
233
234
235@pytest.mark.parametrize("cmake_with_sdist", [True, False])
236def test_cmake_with_sdist_keyword(cmake_with_sdist, capfd):
237    tmp_dir = _tmpdir('cmake_with_sdist')
238
239    tmp_dir.join('setup.py').write(textwrap.dedent(
240        """
241        from skbuild import setup
242        setup(
243            name="cmake_with_sdist_keyword",
244            version="1.2.3",
245            description="a minimal example package",
246            author='The scikit-build team',
247            license="MIT",
248            cmake_with_sdist={cmake_with_sdist}
249        )
250        """.format(cmake_with_sdist=cmake_with_sdist)
251    ))
252    tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
253        """
254        cmake_minimum_required(VERSION 3.5.0)
255        project(test NONE)
256        install(CODE "execute_process(
257          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
258        """
259    ))
260
261    initialize_git_repo_and_commit(tmp_dir)
262
263    with execute_setup_py(tmp_dir, ['sdist'], disable_languages_test=True):
264        pass
265
266    out, _ = capfd.readouterr()
267
268    if cmake_with_sdist:
269        assert "Generating done" in out
270    else:
271        assert "Generating done" not in out
272
273
274def test_cmake_minimum_required_version_keyword():
275    tmp_dir = _tmpdir('cmake_minimum_required_version')
276
277    tmp_dir.join('setup.py').write(textwrap.dedent(
278        """
279        from skbuild import setup
280        setup(
281            name="cmake_with_sdist_keyword",
282            version="1.2.3",
283            description="a minimal example package",
284            author='The scikit-build team',
285            license="MIT",
286            cmake_minimum_required_version='99.98.97'
287        )
288        """
289    ))
290    tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
291        """
292        cmake_minimum_required(VERSION 3.5.0)
293        project(test NONE)
294        install(CODE "execute_process(
295          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
296        """
297    ))
298
299    try:
300        with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
301            pass
302    except SystemExit as e:
303        # Error is not of type SKBuildError, it is expected to be
304        # raised by distutils.core.setup
305        failed = isinstance(e.code, SKBuildError)
306        message = str(e)
307        assert failed
308        assert "CMake version 99.98.97 or higher is required." in message
309
310
311@pytest.mark.skipif(os.environ.get("CONDA_BUILD", "0") == "1",
312                    reason="running tests expecting network connection in Conda is not possible. "
313                           "See https://github.com/conda/conda/issues/508")
314@pytest.mark.skipif(not is_site_reachable("https://pypi.org/simple/cmake/"),
315                    reason="pypi.org website not reachable")
316@pytest.mark.xfail(sys.platform.startswith("win"),
317                   reason="Timing issue with Ninja & MSVC 2019- needs investigation",
318                   strict=False)
319def test_setup_requires_keyword_include_cmake(mocker, capsys):
320
321    mock_setup = mocker.patch('skbuild.setuptools_wrap.upstream_setup')
322
323    tmp_dir = _tmpdir('setup_requires_keyword_include_cmake')
324
325    setup_requires = ['cmake>=3.10']
326
327    tmp_dir.join('setup.py').write(textwrap.dedent(
328        """
329        from skbuild import setup
330        setup(
331            name="cmake_with_sdist_keyword",
332            version="1.2.3",
333            description="a minimal example package",
334            author='The scikit-build team',
335            license="MIT",
336            setup_requires=[{setup_requires}]
337        )
338        """.format(setup_requires=",".join(["'%s'" % package for package in setup_requires]))
339    ))
340    tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
341        """
342        cmake_minimum_required(VERSION 3.5.0)
343        project(test NONE)
344        install(CODE "execute_process(
345          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
346        """
347    ))
348
349    with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
350        assert mock_setup.call_count == 1
351        setup_kw = mock_setup.call_args[1]
352        assert setup_kw['setup_requires'] == setup_requires
353
354        import cmake
355
356        out, _ = capsys.readouterr()
357        if "Searching for cmake>=3.10" in out:
358            assert cmake.__file__.lower().startswith(str(tmp_dir).lower())
359
360
361@pytest.mark.parametrize("distribution_type", ('pure', 'skbuild'))
362def test_script_keyword(distribution_type, capsys):
363
364    # -------------------------------------------------------------------------
365    #
366    # "SOURCE" tree layout for "pure" distribution:
367    #
368    # ROOT/
369    #     setup.py
370    #     foo.py
371    #     bar.py
372    #
373    # "SOURCE" tree layout for "pure" distribution:
374    #
375    # ROOT/
376    #     setup.py
377    #     CMakeLists.txt
378    #
379    # -------------------------------------------------------------------------
380    # "BINARY" distribution layout is identical for both
381    #
382    # ROOT/
383    #     foo.py
384    #     bar.py
385    #
386
387    tmp_dir = _tmpdir('script_keyword')
388
389    tmp_dir.join('setup.py').write(textwrap.dedent(
390        """
391        from skbuild import setup
392        setup(
393            name="test_script_keyword",
394            version="1.2.3",
395            description="a package testing use of script keyword",
396            author='The scikit-build team',
397            license="MIT",
398            scripts=['foo.py', 'bar.py']
399        )
400        """
401    ))
402
403    if distribution_type == 'skbuild':
404        tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
405            """
406            cmake_minimum_required(VERSION 3.5.0)
407            project(foo NONE)
408            file(WRITE "${CMAKE_BINARY_DIR}/foo.py" "# foo.py")
409            file(WRITE "${CMAKE_BINARY_DIR}/bar.py" "# bar.py")
410            install(
411                FILES
412                    "${CMAKE_BINARY_DIR}/foo.py"
413                    "${CMAKE_BINARY_DIR}/bar.py"
414                DESTINATION "."
415                )
416            """
417        ))
418
419        messages = [
420            "copying {}/{}.py -> "
421            "{}/setuptools/scripts-".format(CMAKE_INSTALL_DIR(), module, SKBUILD_DIR())
422            for module in ['foo', 'bar']]
423
424    elif distribution_type == 'pure':
425        tmp_dir.join('foo.py').write("# foo.py")
426        tmp_dir.join('bar.py').write("# bar.py")
427
428        messages = [
429            "copying {}.py -> "
430            "{}/setuptools/scripts-".format(module, SKBUILD_DIR())
431            for module in ['foo', 'bar']]
432
433    with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
434        pass
435
436    out, _ = capsys.readouterr()
437    for message in messages:
438        assert to_platform_path(message) in out
439
440
441@pytest.mark.parametrize("distribution_type", ('pure', 'skbuild'))
442def test_py_modules_keyword(distribution_type, capsys):
443
444    # -------------------------------------------------------------------------
445    #
446    # "SOURCE" tree layout for "pure" distribution:
447    #
448    # ROOT/
449    #     setup.py
450    #     foo.py
451    #     bar.py
452    #
453    # "SOURCE" tree layout for "skbuild" distribution:
454    #
455    # ROOT/
456    #     setup.py
457    #     CMakeLists.txt
458    #
459    # -------------------------------------------------------------------------
460    # "BINARY" distribution layout is identical for both
461    #
462    # ROOT/
463    #     foo.py
464    #     bar.py
465    #
466
467    tmp_dir = _tmpdir('py_modules_keyword')
468
469    tmp_dir.join('setup.py').write(textwrap.dedent(
470        """
471        from skbuild import setup
472        setup(
473            name="test_py_modules_keyword",
474            version="1.2.3",
475            description="a package testing use of py_modules keyword",
476            author='The scikit-build team',
477            license="MIT",
478            py_modules=['foo', 'bar']
479        )
480        """
481    ))
482
483    if distribution_type == 'skbuild':
484        tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
485            """
486            cmake_minimum_required(VERSION 3.5.0)
487            project(foobar NONE)
488            file(WRITE "${CMAKE_BINARY_DIR}/foo.py" "# foo.py")
489            file(WRITE "${CMAKE_BINARY_DIR}/bar.py" "# bar.py")
490            install(
491                FILES
492                    "${CMAKE_BINARY_DIR}/foo.py"
493                    "${CMAKE_BINARY_DIR}/bar.py"
494                DESTINATION "."
495                )
496            """
497        ))
498
499        messages = [
500            "copying {}/{}.py -> "
501            "{}/setuptools/lib".format(CMAKE_INSTALL_DIR(), module, SKBUILD_DIR())
502            for module in ['foo', 'bar']]
503
504    elif distribution_type == 'pure':
505        tmp_dir.join('foo.py').write("# foo.py")
506        tmp_dir.join('bar.py').write("# bar.py")
507
508        messages = [
509            "copying {}.py -> "
510            "{}/setuptools/lib".format(module, SKBUILD_DIR())
511            for module in ['foo', 'bar']]
512
513    with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
514        pass
515
516    out, _ = capsys.readouterr()
517    for message in messages:
518        assert to_platform_path(message) in out
519
520
521@pytest.mark.parametrize("package_parts, module_file, expected", [
522    ([], "", ""),
523    ([""], "file.py", "file.py"),
524    ([], "foo/file.py", "foo/file.py"),
525    (["foo"], "", ""),
526    (["foo"], "", ""),
527    (["foo"], "foo/file.py", "file.py"),
528    (["foo"], "foo\\file.py", "file.py"),
529    (["foo", "bar"], "foo/file.py", "foo/file.py"),
530    (["foo", "bar"], "foo/bar/file.py", "file.py"),
531    (["foo", "bar"], "foo/bar/baz/file.py", "baz/file.py"),
532    (["foo"], "/foo/file.py", "/foo/file.py"),
533])
534def test_strip_package(package_parts, module_file, expected):
535    assert strip_package(package_parts, module_file) == expected
536
537
538@pytest.mark.parametrize("has_cmake_package", [0, 1])  # noqa: C901
539@pytest.mark.parametrize("has_cmake_module", [0, 1])
540@pytest.mark.parametrize("has_hybrid_package", [0, 1])
541@pytest.mark.parametrize("has_pure_package", [0, 1])
542@pytest.mark.parametrize("has_pure_module", [0, 1])
543@pytest.mark.parametrize("with_package_base", [0, 1])
544def test_setup_inputs(
545        has_cmake_package, has_cmake_module,
546        has_hybrid_package,
547        has_pure_package, has_pure_module,
548        with_package_base,
549        mocker):
550    """This test that a project can have a package with some modules
551    installed using setup.py and some other modules installed using CMake.
552    """
553
554    tmp_dir = _tmpdir('test_setup_inputs')
555
556    package_base = 'to/the/base' if with_package_base else ''
557    package_base_dir = package_base + '/' if package_base else ''
558    cmake_source_dir = package_base
559
560    if cmake_source_dir and (has_cmake_package or has_cmake_module):
561        pytest.skip("unsupported configuration: "
562                    "python package fully generated by CMake does *NOT* work. "
563                    "At least __init__.py should be in the project source tree")
564
565    # -------------------------------------------------------------------------
566    # Here is the "SOURCE" tree layout:
567    #
568    # ROOT/
569    #
570    #     setup.py
571    #
572    #     [<base>/]
573    #
574    #         pureModule.py
575    #
576    #         pure/
577    #             __init__.py
578    #             pure.py
579    #
580    #             data/
581    #                 pure.dat
582    #
583    #     [<cmake_src_dir>/]
584    #
585    #         hybrid/
586    #             CMakeLists.txt
587    #             __init__.py
588    #             hybrid_pure.dat
589    #             hybrid_pure.py
590    #
591    #             data/
592    #                 hybrid_data_pure.dat
593    #
594    #             hybrid_2/
595    #                 __init__.py
596    #                 hybrid_2_pure.py
597    #
598    #             hybrid_2_pure/
599    #                 __init__.py
600    #                 hybrid_2_pure_1.py
601    #                 hybrid_2_pure_2.py
602    #
603    #
604    # -------------------------------------------------------------------------
605    # and here is the "BINARY" distribution layout:
606    #
607    # The comment "CMake" or "Setuptools" indicates which tool is responsible
608    # for placing the file in the tree used to create the binary distribution.
609    #
610    # ROOT/
611    #
612    #     cmakeModule.py                 # CMake
613    #
614    #     cmake/
615    #         __init__.py                # CMake
616    #         cmake.py                   # CMake
617    #
618    #     hybrid/
619    #         hybrid_cmake.dat           # CMake
620    #         hybrid_cmake.py            # CMake
621    #         hybrid_pure.dat            # Setuptools
622    #         hybrid_pure.py             # Setuptools
623    #
624    #         data/
625    #             hybrid_data_pure.dat   # CMake or Setuptools
626    #             hybrid_data_cmake.dat  # CMake                  *NO TEST*
627    #
628    #         hybrid_2/
629    #             __init__.py            # CMake or Setuptools
630    #             hybrid_2_pure.py       # CMake or Setuptools
631    #             hybrid_2_cmake.py      # CMake
632    #
633    #         hybrid_2_pure/
634    #             __init__.py            # CMake or Setuptools
635    #             hybrid_2_pure_1.py     # CMake or Setuptools
636    #             hybrid_2_pure_2.py     # CMake or Setuptools
637    #
638    #     pureModule.py                  # Setuptools
639    #
640    #     pure/
641    #         __init__.py                # Setuptools
642    #         pure.py                    # Setuptools
643    #
644    #         data/
645    #             pure.dat               # Setuptools
646
647    tmp_dir.join('setup.py').write(textwrap.dedent(
648        """
649        from skbuild import setup
650        #from setuptools import setup
651        setup(
652            name="test_hybrid_project",
653            version="1.2.3",
654            description=("an hybrid package mixing files installed by both "
655                        "CMake and setuptools"),
656            author='The scikit-build team',
657            license="MIT",
658            cmake_source_dir='{cmake_source_dir}',
659            cmake_install_dir='{cmake_install_dir}',
660            # Arbitrary order of packages
661            packages=[
662        {p_off}    'pure',
663        {h_off}    'hybrid.hybrid_2',
664        {h_off}    'hybrid',
665        {c_off}    'cmake',
666        {p_off}    'hybrid.hybrid_2_pure',
667            ],
668            py_modules=[
669        {pm_off}   '{package_base}pureModule',
670        {cm_off}   '{package_base}cmakeModule',
671            ],
672            package_data={{
673        {p_off}        'pure': ['data/pure.dat'],
674        {h_off}        'hybrid': ['hybrid_pure.dat', 'data/hybrid_data_pure.dat'],
675            }},
676            # Arbitrary order of package_dir
677            package_dir = {{
678        {p_off}    'hybrid.hybrid_2_pure': '{package_base}hybrid/hybrid_2_pure',
679        {p_off}    'pure': '{package_base}pure',
680        {h_off}    'hybrid': '{package_base}hybrid',
681        {h_off}    'hybrid.hybrid_2': '{package_base}hybrid/hybrid_2',
682        {c_off}    'cmake': '{package_base}cmake',
683            }}
684        )
685        """.format(
686            cmake_source_dir=cmake_source_dir,
687            cmake_install_dir=package_base,
688            package_base=package_base_dir,
689            c_off='' if has_cmake_package else '#',
690            cm_off='' if has_cmake_module else '#',
691            h_off='' if has_hybrid_package else '#',
692            p_off='' if has_pure_package else '#',
693            pm_off='' if has_pure_module else '#'
694        )
695    ))
696
697    src_dir = tmp_dir.ensure(package_base, dir=1)
698
699    src_dir.join('CMakeLists.txt').write(textwrap.dedent(
700        """
701        cmake_minimum_required(VERSION 3.5.0)
702        project(hybrid NONE)
703        set(build_dir ${{CMAKE_BINARY_DIR}})
704
705        {c_off} file(WRITE ${{build_dir}}/__init__.py "")
706        {c_off} file(WRITE ${{build_dir}}/cmake.py "")
707        {c_off} install(
708        {c_off}     FILES
709        {c_off}         ${{build_dir}}/__init__.py
710        {c_off}         ${{build_dir}}/cmake.py
711        {c_off}     DESTINATION cmake
712        {c_off}     )
713
714        {cm_off} file(WRITE ${{build_dir}}/cmakeModule.py "")
715        {cm_off} install(
716        {cm_off}     FILES ${{build_dir}}/cmakeModule.py
717        {cm_off}     DESTINATION .)
718
719        {h_off} file(WRITE ${{build_dir}}/hybrid_cmake.dat "")
720        {h_off} install(
721        {h_off}     FILES ${{build_dir}}/hybrid_cmake.dat
722        {h_off}     DESTINATION hybrid)
723
724        {h_off} file(WRITE ${{build_dir}}/hybrid_cmake.py "")
725        {h_off} install(
726        {h_off}     FILES ${{build_dir}}/hybrid_cmake.py
727        {h_off}     DESTINATION hybrid)
728
729        {h_off} file(WRITE ${{build_dir}}/hybrid_data_cmake.dat "")
730        {h_off} install(
731        {h_off}     FILES ${{build_dir}}/hybrid_data_cmake.dat
732        {h_off}     DESTINATION hybrid/data)
733
734        {h_off} file(WRITE ${{build_dir}}/hybrid_2_cmake.py "")
735        {h_off} install(
736        {h_off}     FILES ${{build_dir}}/hybrid_2_cmake.py
737        {h_off}     DESTINATION hybrid/hybrid_2)
738
739        install(CODE "message(STATUS \\\"Installation complete\\\")")
740        """.format(
741            c_off='' if has_cmake_package else '#',
742            cm_off='' if has_cmake_module else '#',
743            h_off='' if has_hybrid_package else '#',
744        )
745    ))
746
747    # List path types: 'c', 'cm', 'h', 'p' or 'pm'
748    try:
749        path_types = list(zip(*filter(
750            lambda i: i[1],
751            [('c', has_cmake_package),
752             ('cm', has_cmake_module),
753             ('h', has_hybrid_package),
754             ('p', has_pure_package),
755             ('pm', has_pure_module)])))[0]
756    except IndexError:
757        path_types = []
758
759    def select_paths(annotated_paths):
760        """Return a filtered list paths considering ``path_types``.
761
762        `annotated_paths`` is list of tuple ``(type, path)`` where type
763        is either `c`, 'cm', `h`, `p` or 'pm'.
764
765        """
766        return filter(lambda i: i[0] in path_types, annotated_paths)
767
768    # Commented paths are the one expected to be installed by CMake. For
769    # this reason, corresponding files should NOT be created in the source
770    # tree.
771    for (_type, path) in select_paths([
772        # ('c', 'cmake/__init__.py'),
773        # ('c', 'cmake/cmake.py'),
774
775        # ('cm', 'cmakeModule.py'),
776
777        ('h', 'hybrid/__init__.py'),
778        # ('h', 'hybrid/hybrid_cmake.dat'),
779        # ('h', 'hybrid/hybrid_cmake.py'),
780        ('h', 'hybrid/hybrid_pure.dat'),
781        ('h', 'hybrid/hybrid_pure.py'),
782
783        # ('h', 'hybrid/data/hybrid_data_cmake.dat'),
784        ('h', 'hybrid/data/hybrid_data_pure.dat'),
785
786        ('h', 'hybrid/hybrid_2/__init__.py'),
787        # ('h', 'hybrid/hybrid_2/hybrid_2_cmake.py'),
788        ('h', 'hybrid/hybrid_2/hybrid_2_pure.py'),
789
790        ('p', 'hybrid/hybrid_2_pure/__init__.py'),
791        ('p', 'hybrid/hybrid_2_pure/hybrid_2_pure_1.py'),
792        ('p', 'hybrid/hybrid_2_pure/hybrid_2_pure_2.py'),
793
794        ('pm', 'pureModule.py'),
795
796        ('p', 'pure/__init__.py'),
797        ('p', 'pure/pure.py'),
798
799        ('p', 'pure/data/pure.dat'),
800    ]):
801        assert _type in ['p', 'pm', 'h']
802        root = (package_base
803                if (_type == 'p' or _type == 'pm')
804                else cmake_source_dir)
805        tmp_dir.ensure(os.path.join(root, path))
806
807    # Do not call the real setup function. Instead, replace it with
808    # a MagicMock allowing to check with which arguments it was invoked.
809    mock_setup = mocker.patch('skbuild.setuptools_wrap.upstream_setup')
810
811    # Convenience print function
812    def _pprint(desc, value=None):
813        print(
814            "-----------------\n"
815            "{}:\n"
816            "\n"
817            "{}\n".format(desc, pprint.pformat(
818                setup_kw.get(desc, {}) if value is None else value, indent=2)))
819
820    with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
821
822        assert mock_setup.call_count == 1
823        setup_kw = mock_setup.call_args[1]
824
825        # packages
826        expected_packages = []
827        if has_cmake_package:
828            expected_packages += ['cmake']
829        if has_hybrid_package:
830            expected_packages += ['hybrid', 'hybrid.hybrid_2']
831        if has_pure_package:
832            expected_packages += ['hybrid.hybrid_2_pure', 'pure']
833
834        _pprint('expected_packages', expected_packages)
835        _pprint('packages')
836
837        # package dir
838        expected_package_dir = {
839            package: (os.path.join(CMAKE_INSTALL_DIR(),
840                      package_base,
841                      package.replace('.', '/')))
842            for package in expected_packages
843            }
844        _pprint('expected_package_dir', expected_package_dir)
845        _pprint('package_dir')
846
847        # package data
848        expected_package_data = {}
849
850        if has_cmake_package:
851            expected_package_data['cmake'] = [
852                '__init__.py',
853                'cmake.py'
854            ]
855
856        if has_hybrid_package:
857            expected_package_data['hybrid'] = [
858                '__init__.py',
859                'hybrid_cmake.dat',
860                'hybrid_cmake.py',
861                'hybrid_pure.dat',
862                'hybrid_pure.py',
863                'data/hybrid_data_cmake.dat',
864                'data/hybrid_data_pure.dat',
865            ]
866            expected_package_data['hybrid.hybrid_2'] = [
867                '__init__.py',
868                'hybrid_2_cmake.py',
869                'hybrid_2_pure.py'
870            ]
871
872        if has_pure_package:
873            expected_package_data['hybrid.hybrid_2_pure'] = \
874                [
875                    '__init__.py',
876                    'hybrid_2_pure_1.py',
877                    'hybrid_2_pure_2.py'
878                ]
879            expected_package_data['pure'] = [
880                '__init__.py',
881                'pure.py',
882                'data/pure.dat',
883            ]
884
885        if has_cmake_module or has_pure_module:
886            expected_modules = []
887            if has_cmake_module:
888                expected_modules.append(package_base_dir + 'cmakeModule.py')
889            if has_pure_module:
890                expected_modules.append(package_base_dir + 'pureModule.py')
891            expected_package_data[''] = expected_modules
892
893        _pprint('expected_package_data', expected_package_data)
894        package_data = {p: sorted(files)
895                        for p, files in setup_kw['package_data'].items()}
896
897        _pprint('package_data', package_data)
898
899        # py_modules (corresponds to files associated with empty package)
900        expected_py_modules = []
901        if '' in expected_package_data:
902            expected_py_modules = [
903                os.path.splitext(module_file)[0]
904                for module_file in expected_package_data['']]
905        _pprint('expected_py_modules', expected_py_modules)
906        _pprint('py_modules')
907
908        # scripts
909        expected_scripts = []
910        _pprint('expected_scripts', expected_scripts)
911        _pprint('scripts')
912
913        # data_files
914        expected_data_files = []
915        _pprint('expected_data_files', expected_data_files)
916        _pprint('data_files')
917
918        assert sorted(setup_kw['packages']) == sorted(expected_packages)
919        assert sorted(setup_kw['package_dir']) == sorted(expected_package_dir)
920        assert package_data == {p: sorted(files)
921                                for p, files in expected_package_data.items()}
922        assert sorted(setup_kw['py_modules']) == sorted(expected_py_modules)
923        assert sorted(setup_kw['scripts']) == sorted([])
924        assert sorted(setup_kw['data_files']) == sorted([])
925
926
927@pytest.mark.parametrize("with_cmake_source_dir", [0, 1])
928def test_cmake_install_into_pure_package(with_cmake_source_dir, capsys):
929
930    # -------------------------------------------------------------------------
931    # "SOURCE" tree layout:
932    #
933    # (1) with_cmake_source_dir == 0
934    #
935    # ROOT/
936    #
937    #     CMakeLists.txt
938    #     setup.py
939    #
940    #     fruits/
941    #         __init__.py
942    #
943    #
944    # (2) with_cmake_source_dir == 1
945    #
946    # ROOT/
947    #
948    #     setup.py
949    #
950    #     fruits/
951    #         __init__.py
952    #
953    #     src/
954    #
955    #         CMakeLists.txt
956    #
957    # -------------------------------------------------------------------------
958    # "BINARY" distribution layout:
959    #
960    # ROOT/
961    #
962    #     fruits/
963    #
964    #         __init__.py
965    #         apple.py
966    #         banana.py
967    #
968    #             data/
969    #
970    #                 apple.dat
971    #                 banana.dat
972    #
973
974    tmp_dir = _tmpdir('cmake_install_into_pure_package')
975
976    cmake_source_dir = 'src' if with_cmake_source_dir else ''
977
978    tmp_dir.join('setup.py').write(textwrap.dedent(
979        """
980        from skbuild import setup
981        setup(
982            name="test_py_modules_keyword",
983            version="1.2.3",
984            description="a package testing use of py_modules keyword",
985            author='The scikit-build team',
986            license="MIT",
987            packages=['fruits'],
988            cmake_install_dir='fruits',
989            cmake_source_dir='{cmake_source_dir}',
990        )
991        """.format(cmake_source_dir=cmake_source_dir)
992    ))
993
994    cmake_src_dir = tmp_dir.ensure(cmake_source_dir, dir=1)
995    cmake_src_dir.join('CMakeLists.txt').write(textwrap.dedent(
996        """
997        cmake_minimum_required(VERSION 3.5.0)
998        project(test NONE)
999        file(WRITE "${CMAKE_BINARY_DIR}/apple.py" "# apple.py")
1000        file(WRITE "${CMAKE_BINARY_DIR}/banana.py" "# banana.py")
1001        install(
1002            FILES
1003                "${CMAKE_BINARY_DIR}/apple.py"
1004                "${CMAKE_BINARY_DIR}/banana.py"
1005            DESTINATION "."
1006            )
1007        file(WRITE "${CMAKE_BINARY_DIR}/apple.dat" "# apple.dat")
1008        file(WRITE "${CMAKE_BINARY_DIR}/banana.dat" "# banana.dat")
1009        install(
1010            FILES
1011                "${CMAKE_BINARY_DIR}/apple.dat"
1012                "${CMAKE_BINARY_DIR}/banana.dat"
1013            DESTINATION "data"
1014            )
1015        """
1016    ))
1017
1018    tmp_dir.ensure('fruits/__init__.py')
1019
1020    with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
1021        pass
1022
1023    messages = [
1024        "copying {}/{} -> "
1025        "{}/setuptools/lib".format(CMAKE_INSTALL_DIR(), module, SKBUILD_DIR())
1026        for module in [
1027            'fruits/__init__.py',
1028            'fruits/apple.py',
1029            'fruits/banana.py',
1030            'fruits/data/apple.dat',
1031            'fruits/data/banana.dat',
1032        ]]
1033
1034    out, _ = capsys.readouterr()
1035    for message in messages:
1036        assert to_platform_path(message) in out
1037
1038
1039@pytest.mark.parametrize("zip_safe", [None, False, True])
1040def test_zip_safe_default(zip_safe, mocker):
1041
1042    mock_setup = mocker.patch('skbuild.setuptools_wrap.upstream_setup')
1043
1044    tmp_dir = _tmpdir('zip_safe_default')
1045
1046    setup_kwarg = ''
1047    if zip_safe is not None:
1048        setup_kwarg = 'zip_safe={}'.format(zip_safe)
1049
1050    tmp_dir.join('setup.py').write(textwrap.dedent(
1051        """
1052        from skbuild import setup
1053        setup(
1054            name="zip_safe_default",
1055            version="1.2.3",
1056            description="a minimal example package",
1057            author='The scikit-build team',
1058            license="MIT",
1059            {setup_kwarg}
1060        )
1061        """.format(setup_kwarg=setup_kwarg)
1062    ))
1063    tmp_dir.join('CMakeLists.txt').write(textwrap.dedent(
1064        """
1065        cmake_minimum_required(VERSION 3.5.0)
1066        project(test NONE)
1067        install(CODE "execute_process(
1068          COMMAND \\${CMAKE_COMMAND} -E sleep 0)")
1069        """
1070    ))
1071
1072    with execute_setup_py(tmp_dir, ['build'], disable_languages_test=True):
1073        pass
1074
1075    assert mock_setup.call_count == 1
1076    setup_kw = mock_setup.call_args[1]
1077
1078    assert "zip_safe" in setup_kw
1079    if zip_safe is None:
1080        assert not setup_kw["zip_safe"]
1081    elif zip_safe:
1082        assert setup_kw["zip_safe"]
1083    else:  # zip_safe is False
1084        assert not setup_kw["zip_safe"]
1085