From 101c62465bdfa46ce8144120e8756efe62398c91 Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Thu, 23 Jul 2020 00:47:51 +0200 Subject: [PATCH 1/8] Inital switch to pytest. More tests and bug fixes. --- .appveyor.yml | 3 +- .travis.yml | 1 + clang_build/build_type.py | 22 ++++++++++--- clang_build/clang_build.py | 20 +++++++---- clang_build/directories.py | 23 ++++++------- setup.cfg | 5 ++- setup.py | 3 +- test/__init__.py | 0 test/test_build_type.py | 4 +++ test/test_circle.py | 7 ++++ test/test_cli.py | 53 ++++++++++++++++++++++++++++++ test/test_directories.py | 33 +++++++++++++++++++ test/{test.py => test_projects.py} | 11 +------ 13 files changed, 150 insertions(+), 35 deletions(-) delete mode 100644 test/__init__.py create mode 100644 test/test_build_type.py create mode 100644 test/test_circle.py create mode 100644 test/test_cli.py create mode 100644 test/test_directories.py rename test/{test.py => test_projects.py} (96%) diff --git a/.appveyor.yml b/.appveyor.yml index c2e3538..b236709 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -18,11 +18,12 @@ install: # Install python packages - "%PYTHON%/Scripts/pip.exe install twine" - "%PYTHON%/Scripts/pip.exe install codecov" + - "%PYTHON%/Scripts/pip.exe install pytest" test_script: # test/hello - "%PYTHON%/python setup.py develop" - - "%PYTHON%/Scripts/coverage run test/test.py" + - "%PYTHON%/Scripts/coverage run -m pytest test" - "%PYTHON%/Scripts/coverage combine" - "%PYTHON%/Scripts/codecov" #- "%PYTHON%/python setup.py develop" diff --git a/.travis.yml b/.travis.yml index feb6b53..aa44dc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,6 +46,7 @@ before_install: install: - pip3 install setuptools - pip3 install codecov + - pip3 install pytest - python3 setup.py develop script: diff --git a/clang_build/build_type.py b/clang_build/build_type.py index 6d01b03..ee711b8 100644 --- a/clang_build/build_type.py +++ b/clang_build/build_type.py @@ -1,18 +1,30 @@ from enum import Enum as _Enum + class BuildType(_Enum): - Default = 'default' - Release = 'release' - RelWithDebInfo = 'relwithdebinfo' - Debug = 'debug' - Coverage = 'coverage' + """Enumeration of all build types. + + Construction is case sensitive, i.e. + + >>> BuildType("default") == BuildType("Default") + True + + """ + + Default = "default" + Release = "release" + RelWithDebInfo = "relwithdebinfo" + Debug = "debug" + Coverage = "coverage" def __str__(self): + """Return the value of the BuildType.""" return self.value # Let's be case insensitive @classmethod def _missing_(cls, value): + """Check for identical value except for caseing.""" for item in cls: if item.value.lower() == value.lower(): return item diff --git a/clang_build/clang_build.py b/clang_build/clang_build.py index f81b303..ac61ee6 100644 --- a/clang_build/clang_build.py +++ b/clang_build/clang_build.py @@ -44,6 +44,12 @@ def _setup_logger(log_level=None): ch.setFormatter(formatter) _LOGGER.addHandler(ch) +def _check_positive(value): + value = int(value) + if value <= 0: + raise _argparse.ArgumentTypeError("%s is negative") + + return value def parse_args(args): _command_line_description = ( @@ -60,6 +66,7 @@ def parse_args(args): action='store_true') parser.add_argument('-d', '--directory', type=_Path, + default=_Path(), help='set the root source directory') parser.add_argument('-b', '--build-type', choices=list(_BuildType), @@ -78,7 +85,7 @@ def parse_args(args): help='also build sources which have already been built', action='store_true') parser.add_argument('-j', '--jobs', - type=int, + type=_check_positive, default=1, help='set the number of concurrent build jobs') parser.add_argument('--debug', @@ -103,14 +110,15 @@ def build(args): # Create container of environment variables environment = _Environment(vars(args)) - categories = ['Configure', 'Compile', 'Link'] - if environment.bundle: - categories.append('Generate bundle') - if environment.redistributable: - categories.append('Generate redistributable') + categories = ['Configure', 'Build'] + #if environment.bundle: + # categories.append('Generate bundle') + #if environment.redistributable: + # categories.append('Generate redistributable') with _CategoryProgress(categories, not args.progress) as progress_bar: project = _Project.from_directory(args.directory, environment) + progress_bar.update() project.build(args.all, args.targets, args.jobs) progress_bar.update() diff --git a/clang_build/directories.py b/clang_build/directories.py index 1e833ee..f5593da 100644 --- a/clang_build/directories.py +++ b/clang_build/directories.py @@ -1,4 +1,14 @@ +from copy import copy + class Directories: + + def include_public_total(self): + includes = copy(self.include_public) + for target in self.dependencies: + includes += target.directories.include_public_total() + + return includes + def __init__(self, files, dependencies): self.dependencies = dependencies @@ -6,13 +16,6 @@ def __init__(self, files, dependencies): self.include_private = files["include_directories"] self.include_public = files["include_directories_public"] - # Default include path - # if self.root_directory.joinpath('include').exists(): - # self._include_directories_public = [self.root_directory.joinpath('include')] + self._include_directories_public - - # Public include directories of dependencies are forwarded - for target in self.dependencies: - self.include_public += target.directories.include_public # Make unique and resolve self.include_private = list( @@ -23,9 +26,7 @@ def __init__(self, files, dependencies): ) def final_directories_list(self): - return list(dict.fromkeys( - self.include_private + self.include_public - )) + return list(dict.fromkeys(self.include_private + self.include_public_total())) def include_command(self): include_directories_command = [] @@ -36,4 +37,4 @@ def include_command(self): def make_private_directories_public(self): self.include_public = self.final_directories_list() - self.include_private = [] \ No newline at end of file + self.include_private = [] diff --git a/setup.cfg b/setup.cfg index 0446aca..0ef94bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,4 +12,7 @@ packages = clang_build [entry_points] console_scripts = - clang-build = clang_build.clang_build:_main \ No newline at end of file + clang-build = clang_build.clang_build:_main + +[aliases] +test=pytest \ No newline at end of file diff --git a/setup.py b/setup.py index c39069a..2c7d500 100644 --- a/setup.py +++ b/setup.py @@ -8,4 +8,5 @@ setuptools.setup( python_requires='>=3.7', setup_requires=['pbr<4'], - pbr=True) + pbr=True, + tests_require=['pytest']) diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/test/test_build_type.py b/test/test_build_type.py new file mode 100644 index 0000000..1ac4032 --- /dev/null +++ b/test/test_build_type.py @@ -0,0 +1,4 @@ +from clang_build.build_type import BuildType + +def test_case_insensitivity(): + assert BuildType("default") == BuildType("Default") diff --git a/test/test_circle.py b/test/test_circle.py new file mode 100644 index 0000000..02aebc1 --- /dev/null +++ b/test/test_circle.py @@ -0,0 +1,7 @@ +from clang_build.circle import Circle + +def test_string_representation(): + c = Circle(["A", "B", "C"]) + + assert str(c) == "A -> B -> C" + assert repr(c) == f'{repr("A")} -> {repr("B")} -> {repr("C")}' diff --git a/test/test_cli.py b/test/test_cli.py new file mode 100644 index 0000000..d1adaa0 --- /dev/null +++ b/test/test_cli.py @@ -0,0 +1,53 @@ +from pathlib import Path +import subprocess +from clang_build import clang_build as cli +from clang_build.build_type import BuildType +import pytest + + +def cli_argument_check( + argument, argument_short, default_value, custom_parameter, expected_outcome +): + args_argument = argument.replace("-", "_") + # default check + args = cli.parse_args([]) + assert vars(args)[args_argument] == default_value + + # custom input + args = cli.parse_args([f"--{argument}"] + custom_parameter) + assert vars(args)[args_argument] == expected_outcome + + if argument_short: + args = cli.parse_args([f"-{argument_short}"] + custom_parameter) + assert vars(args)[args_argument] == expected_outcome + + +def test_cli_arguments(): + cli_argument_check("verbose", "V", False, [], True) + cli_argument_check("progress", "p", False, [], True) + cli_argument_check("directory", "d", Path(), ["my_folder"], Path("my_folder")) + cli_argument_check("build-type", "b", BuildType.Default, ["dEbUg"], BuildType.Debug) + cli_argument_check("all", "a", False, [], True) + cli_argument_check( + "targets", "t", None, ["target1", "target2"], ["target1", "target2"] + ) + with pytest.raises(SystemExit): + cli_argument_check("all", "a", False, ["--targets", "target1"], False) + cli_argument_check("force-build", "f", False, [], True) + cli_argument_check("jobs", "j", 1, ["12"], 12) + with pytest.raises(SystemExit): + cli_argument_check("jobs", "j", 1, ["0"], 0) + cli_argument_check("debug", None, False, [], True) + cli_argument_check("no-graph", None, False, [], True) + cli_argument_check("bundle", None, False, [], True) + cli_argument_check("redistributable", None, False, [], True) + + +def test_hello_world_mwe(): + cli.build(cli.parse_args(["-d", "test/mwe"])) + output = ( + subprocess.check_output(["./build/default/bin/main"], stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + assert output == "Hello!" diff --git a/test/test_directories.py b/test/test_directories.py new file mode 100644 index 0000000..023c792 --- /dev/null +++ b/test/test_directories.py @@ -0,0 +1,33 @@ +from pathlib import Path + +from clang_build.directories import Directories + +# correct order of directories +# + + +class MockDependency: + def __init__(self, directories): + self.directories = directories + + +def test_correct_order(): + d = Directories( + {"include_directories": [Path("a")], "include_directories_public": [Path("b")]}, + [ + MockDependency( + Directories( + { + "include_directories": [Path("m/a")], + "include_directories_public": [Path("m/b")], + }, + [], + ) + ) + ], + ) + + assert d.include_public == [Path("b")] + assert d.include_private == [Path("a")] + assert d.include_public_total() == [Path("b"), Path("m/b")] + assert d.include_command() == ["-I", "a", "-I", "b", "-I", str(Path("m/b"))] diff --git a/test/test.py b/test/test_projects.py similarity index 96% rename from test/test.py rename to test/test_projects.py index 6551fff..ea1c5aa 100644 --- a/test/test.py +++ b/test/test_projects.py @@ -37,16 +37,7 @@ def clang_build_try_except( args ): logger.error(printout) class TestClangBuild(unittest.TestCase): - def test_hello_world_mwe(self): - clang_build_try_except(['-d', 'test/mwe']) - - try: - output = subprocess.check_output(['./build/default/bin/main'], stderr=subprocess.STDOUT).decode('utf-8').strip() - except subprocess.CalledProcessError as e: - self.fail(f'Could not run compiled program. Message:\n{e.output}') - - self.assertEqual(output, 'Hello!') - + def test_build_types(self): for build_type in ['release', 'relwithdebinfo', 'debug', 'coverage']: clang_build_try_except(['-d', 'test/mwe', '-b', build_type]) From f72ae51bbb4c7e574dbf0d5050f3f07606050e24 Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sat, 25 Jul 2020 14:47:30 +0200 Subject: [PATCH 2/8] More tests. --- clang_build/directories.py | 2 +- test/test_directories.py | 22 +++++++++++++ test/test_environment.py | 63 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 test/test_environment.py diff --git a/clang_build/directories.py b/clang_build/directories.py index f5593da..ca215de 100644 --- a/clang_build/directories.py +++ b/clang_build/directories.py @@ -36,5 +36,5 @@ def include_command(self): return include_directories_command def make_private_directories_public(self): - self.include_public = self.final_directories_list() + self.include_public = self.include_private + self.include_public self.include_private = [] diff --git a/test/test_directories.py b/test/test_directories.py index 023c792..95e2344 100644 --- a/test/test_directories.py +++ b/test/test_directories.py @@ -31,3 +31,25 @@ def test_correct_order(): assert d.include_private == [Path("a")] assert d.include_public_total() == [Path("b"), Path("m/b")] assert d.include_command() == ["-I", "a", "-I", "b", "-I", str(Path("m/b"))] + assert d.final_directories_list() == [Path("a"), Path("b"), Path("m/b")] + + d.make_private_directories_public() + assert d.include_public == [Path("a"), Path("b")] + assert d.include_private == [] + + +def test_empty_dependency_list(): + d = Directories( + {"include_directories": [Path("a")], "include_directories_public": [Path("b")]}, + [], + ) + + assert d.include_public == [Path("b")] + assert d.include_private == [Path("a")] + assert d.include_public_total() == [Path("b")] + assert d.include_command() == ["-I", "a", "-I", "b"] + assert d.final_directories_list() == [Path("a"), Path("b")] + + d.make_private_directories_public() + assert d.include_public == [Path("a"), Path("b")] + assert d.include_private == [] diff --git a/test/test_environment.py b/test/test_environment.py new file mode 100644 index 0000000..b044416 --- /dev/null +++ b/test/test_environment.py @@ -0,0 +1,63 @@ +from pathlib import Path + +from clang_build.environment import Environment +from clang_build.build_type import BuildType +from clang_build.compiler import Clang + + +def test_compiler(): + env = Environment({}) + assert isinstance(env.compiler, Clang) + + +def test_build_type(): + env = Environment({}) + assert env.build_type == BuildType.Default + + env = Environment({"build-type": BuildType.Release}) + assert env.build_type == BuildType.Release + + +def test_force_build(): + env = Environment({}) + assert env.force_build == False + + env = Environment({"force_build": True}) + assert env.force_build == True + + +def test_build_directory(): + env = Environment({}) + assert env.build_directory == Path("build") + + +def test_create_dependency_dotfile(): + env = Environment({}) + assert env.create_dependency_dotfile == True + + env = Environment({"no_graph": True}) + assert env.create_dependency_dotfile == False + + +def test_clone_recursive(): + env = Environment({}) + assert env.clone_recursive == True + + env = Environment({"no_recursive_clone": True}) + assert env.clone_recursive == False + + +def test_bundle(): + env = Environment({}) + assert env.bundle == False + + env = Environment({"bundle": True}) + assert env.bundle == True + + +def test_redistributable(): + env = Environment({}) + assert env.redistributable == False + + env = Environment({"redistributable": True}) + assert env.redistributable == True From 9f250b100e669738ea598427b780cbce6b60b0c9 Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sat, 25 Jul 2020 15:14:19 +0200 Subject: [PATCH 3/8] Improved C++ standard detection. --- clang_build/compiler.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/clang_build/compiler.py b/clang_build/compiler.py index 8f70c1e..bc984e2 100644 --- a/clang_build/compiler.py +++ b/clang_build/compiler.py @@ -5,7 +5,7 @@ import subprocess as _subprocess from functools import lru_cache as _lru_cache from pathlib import Path as _Path - +from re import search as _search _LOGGER = _logging.getLogger(__name__) @@ -99,7 +99,8 @@ def _get_dialect_flag(self, year): """ return "-std=c++{:02d}".format(year) - def _dialect_exists(self, year, clangpp): + @_lru_cache(maxsize=1) + def dialect_exists(self, year): """Check if a given dialect flag is valid. Parameters @@ -107,8 +108,6 @@ def _dialect_exists(self, year, clangpp): year : int The last two digits of the dialect. For example 11 for `C++11`. - clangpp : :any:`pathlib.Path` - Path to the clang++ executable Returns ------- @@ -120,7 +119,7 @@ def _dialect_exists(self, year, clangpp): std_opt = self._get_dialect_flag(year) try: _subprocess.run( - [str(clangpp), std_opt, "-x", "c++", "-E", "-"], + [str(self.clangpp), std_opt, "-x", "c++", "-E", "-"], check=True, input=b"", stdout=_subprocess.PIPE, @@ -149,12 +148,17 @@ def _get_max_supported_compiler_dialect(self, clangpp): Flag string of the latest supported dialect """ - supported_dialects = [] - for dialect in range(30): - if self._dialect_exists(dialect, clangpp): - supported_dialects.append(dialect) - - if supported_dialects: - return self._get_dialect_flag(max(supported_dialects)) - else: - return self._get_dialect_flag(98) + try: + _subprocess.run( + [str(clangpp), "-std=dummpy", "-x", "c++", "-E", "-"], + check=True, + stdout=_subprocess.PIPE, + stderr=_subprocess.PIPE, + encoding="utf8", + ) + except _subprocess.CalledProcessError as subprocess_error: + for line in subprocess_error.stderr.splitlines(): + if "draft" in line or "gnu" in line: + continue + + return _search(r"'(c\+\+..)'", line).group(1) From ed5dfbf45bc6568b3434a111f9d399273fed5dae Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sat, 25 Jul 2020 15:14:19 +0200 Subject: [PATCH 4/8] Improved C++ standard detection. --- clang_build/compiler.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/clang_build/compiler.py b/clang_build/compiler.py index 8f70c1e..49a7323 100644 --- a/clang_build/compiler.py +++ b/clang_build/compiler.py @@ -5,7 +5,7 @@ import subprocess as _subprocess from functools import lru_cache as _lru_cache from pathlib import Path as _Path - +from re import search as _search _LOGGER = _logging.getLogger(__name__) @@ -99,7 +99,8 @@ def _get_dialect_flag(self, year): """ return "-std=c++{:02d}".format(year) - def _dialect_exists(self, year, clangpp): + @_lru_cache(maxsize=1) + def dialect_exists(self, year): """Check if a given dialect flag is valid. Parameters @@ -107,8 +108,6 @@ def _dialect_exists(self, year, clangpp): year : int The last two digits of the dialect. For example 11 for `C++11`. - clangpp : :any:`pathlib.Path` - Path to the clang++ executable Returns ------- @@ -120,7 +119,7 @@ def _dialect_exists(self, year, clangpp): std_opt = self._get_dialect_flag(year) try: _subprocess.run( - [str(clangpp), std_opt, "-x", "c++", "-E", "-"], + [str(self.clangpp), std_opt, "-x", "c++", "-E", "-"], check=True, input=b"", stdout=_subprocess.PIPE, @@ -149,12 +148,17 @@ def _get_max_supported_compiler_dialect(self, clangpp): Flag string of the latest supported dialect """ - supported_dialects = [] - for dialect in range(30): - if self._dialect_exists(dialect, clangpp): - supported_dialects.append(dialect) - - if supported_dialects: - return self._get_dialect_flag(max(supported_dialects)) - else: - return self._get_dialect_flag(98) + try: + _subprocess.run( + [str(clangpp), "-std=dummpy", "-x", "c++", "-E", "-"], + check=True, + stdout=_subprocess.PIPE, + stderr=_subprocess.PIPE, + encoding="utf8", + ) + except _subprocess.CalledProcessError as subprocess_error: + for line in reversed(subprocess_error.stderr.splitlines()): + if "draft" in line or "gnu" in line: + continue + + return _search(r"'(c\+\+..)'", line).group(1) From dc031a0aba33da9a97064415e224784d963388de Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sat, 25 Jul 2020 15:35:44 +0200 Subject: [PATCH 5/8] More tests. --- test/test_environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_environment.py b/test/test_environment.py index b044416..a4b3c40 100644 --- a/test/test_environment.py +++ b/test/test_environment.py @@ -14,7 +14,7 @@ def test_build_type(): env = Environment({}) assert env.build_type == BuildType.Default - env = Environment({"build-type": BuildType.Release}) + env = Environment({"build_type": BuildType.Release}) assert env.build_type == BuildType.Release From da30876038dbceff155d4a5401dcf2abc9da546f Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sat, 25 Jul 2020 17:38:25 +0200 Subject: [PATCH 6/8] The compiler now compiles and links. --- clang_build/compiler.py | 149 ++++++++++++++++++++++++++++++++--- clang_build/single_source.py | 37 ++------- clang_build/target.py | 32 +++----- 3 files changed, 153 insertions(+), 65 deletions(-) diff --git a/clang_build/compiler.py b/clang_build/compiler.py index 610d4f9..b786618 100644 --- a/clang_build/compiler.py +++ b/clang_build/compiler.py @@ -148,17 +148,144 @@ def _get_max_supported_compiler_dialect(self, clangpp): Flag string of the latest supported dialect """ + _, report = self._run_clang_command( + [str(clangpp), "-std=dummpy", "-x", "c++", "-E", "-"] + ) + + for line in reversed(report.splitlines()): + if "draft" in line or "gnu" in line: + continue + + return "-std=" + _search(r"'(c\+\+..)'", line).group(1) + + def _get_driver(self, source_file): + if source_file.suffix in [".c", ".cc", ".m"]: + return [str(self.clang)] + else: + return [str(self.clangpp), self.max_cpp_dialect] + + def compile(self, source_file, object_file, flags): + """Compile a given source file into an object file. + + If the object file is placed into a non-existing folder, this + folder is generated before compilation. + + Parameters + ---------- + source_file : pathlib.Path + The source file to compile + + object_file : pathlib.Path + The object file to generate during compilation + + flags : list of str + List of flags to pass to the compiler + + Returns + ------- + bool + True if the compilation was successful, else False + str + Output of the compiler + + """ + object_file.parents[0].mkdir(parents=True, exist_ok=True) + + return self._run_clang_command( + self._get_driver(source_file) + + ["-c", str(source_file), "-o", str(object_file)] + + flags + ) + + def generate_dependency_file(self, source_file, dependency_file, flags): + """Generate a dependency file for a given source file. + + If the dependency file is placed into a non-existing folder, this + folder is generated before compilation. + + Parameters + ---------- + source_file : pathlib.Path + The source file to compile + + dependency_file : pathlib.Path + The dependency file to generate + + flags : list of str + List of flags to pass to the compiler + + Returns + ------- + bool + True if the dependency file generation was successful, else False + str + Output of the compiler + + """ + dependency_file.parents[0].mkdir(parents=True, exist_ok=True) + + return self._run_clang_command( + self._get_driver(source_file) + + ["-E", "-MMD", str(source_file), "-MF", str(dependency_file)] + + flags + ) + + _LINKER_OPTIONS = { + "executable": ["-o"], + "shared": ["-shared", "-o"], + "static": ["rc"], + } + + def link(self, output_file, command, output_type): + """Link into the given output_file. + + The command should contain all object files, library search paths + and libraries against which to link. If the output_file is placed + in a non-existing folder, the folder and all required parents + are generated. + + Parameters + ---------- + output_file : pathlib.Path + The output file to generate + command : list of str + All objects files and search paths etc should be in here + output_type : str + One of the following three: "executable", "shared", "static" + + Returns + ------- + bool + True if linking was successful, False otherwise + str + The output of the linker + + Raises + ------ + ValueError + If an output_type other than the allowed ones is passed + + """ + output_file.parents[0].mkdir(parents=True, exist_ok=True) + try: - _subprocess.run( - [str(clangpp), "-std=dummpy", "-x", "c++", "-E", "-"], - check=True, - stdout=_subprocess.PIPE, - stderr=_subprocess.PIPE, - encoding="utf8", + type_flags = self._LINKER_OPTIONS[output_type] + except KeyError: + raise ValueError( + f"Invalid output type: {output_type}. Valid options are: " + + f"{list(self._LINKER_OPTIONS.keys())}" ) - except _subprocess.CalledProcessError as subprocess_error: - for line in reversed(subprocess_error.stderr.splitlines()): - if "draft" in line or "gnu" in line: - continue - return "-std=" + _search(r"'(c\+\+..)'", line).group(1) + return self._run_clang_command( + [str(self.clang_ar)] + type_flags + str(output_file) + command + ) + + def _run_clang_command(self, command): + success = True + try: + report = _subprocess.check_output(command, encoding="utf8").strip() + except _subprocess.CalledProcessError as error: + success = False + report = error.output.strip() + + return success, report diff --git a/clang_build/single_source.py b/clang_build/single_source.py index 74fa408..5a871ee 100644 --- a/clang_build/single_source.py +++ b/clang_build/single_source.py @@ -69,49 +69,22 @@ def __init__( self.object_file = _Path(object_directory, relpath, self.source_file.stem + '.o') self.depfile = _Path(depfile_directory, relpath, self.source_file.stem + '.d') - compiler = str(environment.compiler.clangpp) - max_cpp_dialect = environment.compiler.max_cpp_dialect - # Unset dialect flag and use clang if source file is not C++ - if source_file.suffix in [".c", ".cc", ".m"]: - compiler = str(environment.compiler.clang) - max_cpp_dialect = '' + self.compiler = environment.compiler self.needs_rebuild = _needs_rebuild(self.object_file, self.source_file, self.depfile) - flags = compile_flags + include_strings + self.flags = compile_flags + include_strings + self.platform_flags = platform_flags self.compilation_failed = False - # prepare everything for dependency file generation - self.dependency_command = [compiler, max_cpp_dialect, '-E', '-MMD', str(self.source_file), '-MF', str(self.depfile)] + flags - - # prepare everything for compilation - self.compile_command = [compiler, max_cpp_dialect, '-c', str(self.source_file), '-o', str(self.object_file)] + flags + platform_flags - def generate_depfile(self): - # TODO: logging in multiprocess - # _LOGGER.debug(' ' + ' '.join(dependency_command)) - try: - self.depfile.parents[0].mkdir(parents=True, exist_ok=True) - self.depfile_report = _subprocess.check_output(self.dependency_command, stderr=_subprocess.STDOUT).decode('utf-8').strip() - self.depfile_failed = False - except _subprocess.CalledProcessError as error: - self.depfile_failed = True - self.depfile_report = error.output.decode('utf-8').strip() + self.depfile_failed, self.depfile_report = self.compiler.generate_dependency_file(self.source_file, self.depfile, self.flags) def compile(self): - # TODO: logging in multiprocess - # _LOGGER.debug(' ' + ' '.join(self.compile_command)) - try: - self.object_file.parents[0].mkdir(parents=True, exist_ok=True) - self.compile_report = _subprocess.check_output(self.compile_command, stderr=_subprocess.STDOUT).decode('utf-8').strip() - self.compilation_failed = False - except _subprocess.CalledProcessError as error: - self.compilation_failed = True - self.compile_report = error.output.decode('utf-8').strip() - + self.compilation_failed, self.compile_report = self.compiler.compile(self.source_file, self.object_file, self.flags + self.platform_flags) if __name__ == '__name__': _freeze_support() \ No newline at end of file diff --git a/clang_build/target.py b/clang_build/target.py index 36f0856..329f6b0 100644 --- a/clang_build/target.py +++ b/clang_build/target.py @@ -263,7 +263,7 @@ def __init__( self, target_description, files, - link_command, + link_output_type, output_folder, platform_flags, prefix, @@ -287,6 +287,10 @@ def __init__( self.depfile_directory = (self.build_directory / "dep").resolve() self.output_folder = (self.build_directory / output_folder).resolve() self.redistributable_folder = (self.build_directory / "redistributable").resolve() + self.link_output_type = link_output_type + self.link_command = [] + self.unsuccessful_link = None + self.link_report = None self.outname = target_description.config.get("output_name", self.name) self.outfilename = prefix + self.outname + suffix @@ -314,8 +318,6 @@ def __init__( # If compilation of buildables fail, they will be stored here later self._unsuccessful_compilations = [] - # Linking setup - self.link_command = link_command + [str(self.outfile)] def _get_default_flags(self): """Return the default any:`clang_build.flags.BuildFlags` with compile flags but without link flags. @@ -387,23 +389,9 @@ def compile(self, process_pool, progress_disabled): {self.identifier: [source.compile_report for source in self._unsuccessful_compilations]}) def link(self): - link_command = str(" ".join(dict.fromkeys(self.link_command))) self._logger.info(f'link -> "{self.outfile}"') - self._logger.debug(" " + link_command) - link_command = list(link_command.split()) - - # Execute link command - try: - self.output_folder.mkdir(parents=True, exist_ok=True) - self.link_report = ( - _subprocess.check_output(link_command, stderr=_subprocess.STDOUT) - .decode("utf-8") - .strip() - ) - self.unsuccessful_link = False - except _subprocess.CalledProcessError as error: - self.unsuccessful_link = True - self.link_report = error.output.decode("utf-8").strip() + self.unsuccessful_link, self.link_report = self._environment.compiler.link( + self.outfile, self.link_command, self.link_output_type) # Catch link errors if self.unsuccessful_link: @@ -424,7 +412,7 @@ def __init__(self, target_description, files, dependencies=None): super().__init__( target_description=target_description, files=files, - link_command=[str(target_description.environment.compiler.clangpp), "-o"], + link_output_type="executable", output_folder=_platform.EXECUTABLE_OUTPUT, platform_flags=_platform.PLATFORM_EXTRA_FLAGS_EXECUTABLE, prefix=_platform.EXECUTABLE_PREFIX, @@ -558,7 +546,7 @@ def __init__(self, target_description, files, dependencies=None): super().__init__( target_description=target_description, files=files, - link_command=[str(target_description.environment.compiler.clangpp), "-shared", "-o"], + link_output_type="shared", output_folder=_platform.SHARED_LIBRARY_OUTPUT, platform_flags=_platform.PLATFORM_EXTRA_FLAGS_SHARED, prefix=_platform.SHARED_LIBRARY_PREFIX, @@ -640,7 +628,7 @@ def __init__(self, target_description, files, dependencies=None): super().__init__( target_description=target_description, files=files, - link_command=[str(target_description.environment.compiler.clang_ar), "rc"], + link_output_type="static", output_folder=_platform.STATIC_LIBRARY_OUTPUT, platform_flags=_platform.PLATFORM_EXTRA_FLAGS_STATIC, prefix=_platform.STATIC_LIBRARY_PREFIX, From 9626daf52559e1406dcebb40aa295bbf3830ee3c Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sun, 26 Jul 2020 00:12:38 +0200 Subject: [PATCH 7/8] More compiler tests. --- clang_build/compiler.py | 8 ++-- test/test_compiler.py | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 test/test_compiler.py diff --git a/clang_build/compiler.py b/clang_build/compiler.py index 4cdf8df..f205b2a 100644 --- a/clang_build/compiler.py +++ b/clang_build/compiler.py @@ -147,7 +147,7 @@ def _get_max_supported_compiler_dialect(self): [str(self.clangpp), "-std=dummpy", "-x", "c++", "-E", "-"] ) - for line in reversed(report): + for line in reversed(report.splitlines()): if "draft" in line or "gnu" in line: continue @@ -161,7 +161,7 @@ def _get_driver(self, source_file): else: return [str(self.clangpp), self.max_cpp_dialect] - def compile(self, source_file, object_file, flags): + def compile(self, source_file, object_file, flags=None): """Compile a given source file into an object file. If the object file is placed into a non-existing folder, this @@ -191,7 +191,7 @@ def compile(self, source_file, object_file, flags): return self._run_clang_command( self._get_driver(source_file) + ["-c", str(source_file), "-o", str(object_file)] - + flags + + (flags if flags else []) ) def generate_dependency_file(self, source_file, dependency_file, flags): @@ -285,5 +285,5 @@ def _run_clang_command(self, command): except _subprocess.CalledProcessError as error: success = False report = error.output.strip() - + return success, report diff --git a/test/test_compiler.py b/test/test_compiler.py new file mode 100644 index 0000000..bb3ccca --- /dev/null +++ b/test/test_compiler.py @@ -0,0 +1,87 @@ +from pathlib import Path +import subprocess + +from clang_build.compiler import Clang + + +def test_finds_clang(): + compiler = Clang() + + for exe in [compiler.clang, compiler.clangpp, compiler.clang_ar]: + subprocess.check_call([str(exe), "--version"]) + + +def create_file(content, path): + with open(path, 'w') as f: + f.write(content) + +def remove_file(path): + path.unlink(missing_ok=True) + +def remove_dir(path): + path.rmdir() + + +def test_compile_empty_source(): + compiler = Clang() + source_file = Path("empty.cpp") + object_file = Path("output.o") + try: + create_file("", source_file) + success, _ = compiler.compile(source_file, object_file) + assert object_file.exists() + assert success + finally: + remove_file(object_file) + remove_file(source_file) + +def test_compile_faulty_source(): + compiler = Clang() + source_file = Path("faulty.cpp") + object_file = Path("should_not_be_here.o") + try: + create_file("{", source_file) + success, report = compiler.compile(source_file, object_file) + assert not success + assert str(source_file)+":1:1" in report + finally: + remove_file(object_file) + remove_file(source_file) + +def test_compile_with_flags(): + compiler = Clang() + source_file = Path("needs_flags.cpp") + object_file = Path("should_not_be_here.o") + try: + create_file("int main(){\n#ifdef HIFLAG\n}\n#endif", source_file) + success, _ = compiler.compile(source_file, object_file) + assert not success + success, _ = compiler.compile(source_file, object_file, ["-DHIFLAG"]) + assert success + assert object_file.exists() + finally: + remove_file(object_file) + remove_file(source_file) + +def test_compile_output_in_folder(): + compiler = Clang() + source_file = Path("empty.cpp") + object_file = Path("nested/folder/output.o") + try: + create_file("", source_file) + success, _ = compiler.compile(source_file, object_file) + assert success + assert object_file.exists() + finally: + remove_file(object_file) + remove_dir(object_file.parent) + remove_dir(object_file.parent.parent) + remove_file(source_file) + + +def test_link(): + assert False + + +def test_dependency_file(): + assert False \ No newline at end of file From 7ff292fa65d787674b75241bd94b67689ddc66a3 Mon Sep 17 00:00:00 2001 From: Nicholas Ohs Date: Sun, 26 Jul 2020 04:07:49 +0200 Subject: [PATCH 8/8] Added missing clean up. --- test/test_cli.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/test/test_cli.py b/test/test_cli.py index d1adaa0..ab988f6 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,8 +1,11 @@ -from pathlib import Path import subprocess +from pathlib import Path +from shutil import rmtree + +import pytest + from clang_build import clang_build as cli from clang_build.build_type import BuildType -import pytest def cli_argument_check( @@ -44,10 +47,13 @@ def test_cli_arguments(): def test_hello_world_mwe(): - cli.build(cli.parse_args(["-d", "test/mwe"])) - output = ( - subprocess.check_output(["./build/default/bin/main"], stderr=subprocess.STDOUT) - .decode("utf-8") - .strip() - ) - assert output == "Hello!" + try: + cli.build(cli.parse_args(["-d", "test/mwe"])) + output = ( + subprocess.check_output(["./build/default/bin/main"], stderr=subprocess.STDOUT) + .decode("utf-8") + .strip() + ) + assert output == "Hello!" + finally: + rmtree("build", ignore_errors=True) \ No newline at end of file