diff --git a/test/test_build.py b/test/test_build.py index d2bda0f..4f1bba0 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -98,6 +98,58 @@ def test_unsupported_target(self, linux): build(tree=linux, targets=["unknown-target"]) +class TestMakeTarget: + def test_adds_make_target(self, linux): + b = Build(tree=linux, targets=[], make_target=["drivers/mmc/"]) + names = [t.name for t in b.targets] + assert "make:drivers/mmc/" in names + + def test_pulls_in_config(self, linux): + b = Build(tree=linux, targets=[], make_target=["drivers/mmc/"]) + names = [t.name for t in b.targets] + assert "config" in names + + def test_does_not_pull_defaults(self, linux): + b = Build(tree=linux, targets=[], make_target=["drivers/mmc/"]) + names = [t.name for t in b.targets] + assert "default" not in names + assert "kernel" not in names + + def test_positional_make_target(self, linux): + b = Build(tree=linux, targets=["config", "drivers/dma/"]) + names = [t.name for t in b.targets] + assert "make:drivers/dma/" in names + assert "config" in names + + def test_positional_object_target(self, linux): + b = Build(tree=linux, targets=["config", "kernel/livepatch/patch.o"]) + names = [t.name for t in b.targets] + assert "make:kernel/livepatch/patch.o" in names + + def test_unknown_bareword_still_fails(self, linux): + with pytest.raises(tuxmake.exceptions.UnsupportedTarget): + Build(tree=linux, targets=["typo-target"]) + + def test_multiple_make_targets(self, linux): + b = Build( + tree=linux, + targets=[], + make_target=["drivers/mmc/", "kernel/livepatch/patch.o"], + ) + names = [t.name for t in b.targets] + assert "make:drivers/mmc/" in names + assert "make:kernel/livepatch/patch.o" in names + + def test_no_duplicate_when_positional_and_flag(self, linux): + b = Build( + tree=linux, + targets=["drivers/mmc/"], + make_target=["drivers/mmc/"], + ) + names = [t.name for t in b.targets] + assert names.count("make:drivers/mmc/") == 1 + + class TestKconfig: def test_kconfig_default(self, linux, Popen): b = Build(tree=linux, targets=["config"]) diff --git a/test/test_cli.py b/test/test_cli.py index 3c69a00..3b7aa41 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -70,6 +70,29 @@ def test_config_multiple(self, builder): tuxmake("config", "kernel") assert args(builder).targets == ["config", "kernel"] + def test_make_target(self, builder): + tuxmake("--make-target=drivers/mmc/") + assert args(builder).make_target == ["drivers/mmc/"] + + def test_make_target_with_positional(self, builder): + tuxmake("config", "--make-target=drivers/mmc/") + assert args(builder).targets == ["config"] + assert args(builder).make_target == ["drivers/mmc/"] + + def test_positional_kconfig(self, builder): + tuxmake("defconfig") + assert args(builder).kconfig == "defconfig" + assert not getattr(args(builder), "targets", []) + + def test_positional_kconfig_allmodconfig(self, builder): + tuxmake("allmodconfig") + assert args(builder).kconfig == "allmodconfig" + + def test_explicit_kconfig_wins_over_positional(self, builder): + tuxmake("--kconfig=tinyconfig", "defconfig") + assert args(builder).kconfig == "tinyconfig" + assert "defconfig" not in getattr(args(builder), "targets", []) + class TestMakeVariables: def test_basic(self, builder): diff --git a/test/test_cmdline.py b/test/test_cmdline.py index b766efa..286179c 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -61,6 +61,12 @@ def test_kconfig_add(self, cmdline): assert "--kconfig-add=foo.config" in cmd assert "--kconfig-add=bar.config" in cmd + def test_make_target(self, cmdline): + build = Build(targets=[], make_target=["drivers/mmc/"]) + cmd = cmdline.reproduce(build) + assert "--make-target=drivers/mmc/" in cmd + assert "make:drivers/mmc/" not in cmd + def test_debug(self, cmdline): cmd = cmdline.reproduce(Build(debug=True)) assert "--debug" in cmd diff --git a/test/test_target.py b/test/test_target.py index 7c72979..270a90e 100644 --- a/test/test_target.py +++ b/test/test_target.py @@ -2,7 +2,14 @@ import tuxmake.exceptions from tuxmake.arch import Native -from tuxmake.target import Compression, Target, Config, Kselftest, create_target +from tuxmake.target import ( + Compression, + Target, + Config, + Kselftest, + MakeTarget, + create_target, +) @pytest.fixture @@ -231,6 +238,21 @@ def test_kdir_set_to_build_dir(self, build): assert kselftest.makevars.get("KDIR") == "{build_dir}" +class TestMakeTarget: + @pytest.fixture + def make_target(self, build): + return MakeTarget("drivers/mmc/", build) + + def test_name(self, make_target): + assert make_target.name == "make:drivers/mmc/" + + def test_commands(self, make_target): + assert make_target.commands[0] == ["{make}", "drivers/mmc/"] + + def test_depends_on_config(self, make_target): + assert make_target.dependencies == ["config"] + + class TestCompression: def test_invalid_compression(self): with pytest.raises(tuxmake.exceptions.UnsupportedCompression): diff --git a/tuxmake/build.py b/tuxmake/build.py index b2186a5..cda3a1d 100644 --- a/tuxmake/build.py +++ b/tuxmake/build.py @@ -18,6 +18,7 @@ from tuxmake.output import get_new_output_dir, get_default_korg_toolchains_dir from tuxmake.target import Compression from tuxmake.target import default_compression +from tuxmake.target import MakeTarget from tuxmake.target import create_target from tuxmake.runtime import Runtime, DockerRuntime from tuxmake.runtime import Terminated @@ -181,6 +182,7 @@ def __init__( targets=defaults.targets, compression_type=None, kernel_image=None, + make_target=None, jobs=None, runtime=None, fail_fast=False, @@ -227,7 +229,7 @@ def __init__( self.dynamic_make_variables = dict(self.target_arch.dynamic_makevars) if not targets: - targets = defaults.targets + targets = [] if make_target else defaults.targets if kernel_image: self.target_overrides = {"kernel": kernel_image} @@ -242,6 +244,15 @@ def __init__( self.__ordering_only_targets__ = {} for t in targets: self.add_target(t) + self.make_target = make_target or [] + if self.make_target: + self.add_target("config") + existing = {t.name for t in self.targets} + for mt in self.make_target: + target = MakeTarget(mt, self, self.compression) + if target.name not in existing: + self.__ordering_only_targets__[target.name] = False + self.targets.append(target) self.cleanup_targets() self.extend_kconfig() @@ -301,10 +312,10 @@ def add_target(self, target_name, ordering_only=False): target = create_target(target_name, self, self.compression) if ordering_only: - if target_name not in self.__ordering_only_targets__: - self.__ordering_only_targets__[target_name] = True + if target.name not in self.__ordering_only_targets__: + self.__ordering_only_targets__[target.name] = True else: - self.__ordering_only_targets__[target_name] = False + self.__ordering_only_targets__[target.name] = False for d in target.dependencies: self.add_target(d, ordering_only=ordering_only) diff --git a/tuxmake/cli.py b/tuxmake/cli.py index 7be0ee5..6ee116a 100644 --- a/tuxmake/cli.py +++ b/tuxmake/cli.py @@ -1,6 +1,7 @@ from datetime import timedelta import os import pathlib +import re import subprocess import shlex import sys @@ -141,6 +142,14 @@ def format_yes_no(b, length): options.make_variables = dict((arg.split("=") for arg in key_values)) options.targets = [arg for arg in options.targets if "=" not in arg] + kconfig_re = re.compile(r"^[\w\-]+config$") + for arg in list(options.targets): + if kconfig_re.match(arg): + if not options.kconfig: + options.kconfig = arg + options.targets.remove(arg) + break + build_args = { k: v for k, v in options.__dict__.items() diff --git a/tuxmake/cmdline.py b/tuxmake/cmdline.py index 5e3b3cc..4baaa26 100644 --- a/tuxmake/cmdline.py +++ b/tuxmake/cmdline.py @@ -104,6 +104,14 @@ def build_parser(cls=argparse.ArgumentParser, **kwargs): type=str, help="Kernel image to build, overriding the default image name for the target architecture.", ) + target.add_argument( + "-M", + "--make-target", + type=str, + action="append", + default=[], + help="Arbitrary Kbuild target to pass to make, e.g. drivers/mmc/ or kernel/livepatch/patch.o. Can be specified multiple times. Kbuild-like positional targets (with / or .o/.ko extensions) also work without this flag.", + ) buildenv = parser.add_argument_group("Build environment options") buildenv.add_argument( @@ -329,6 +337,8 @@ def reproduce(self, build): for k, v in build.make_variables.items(): cmd.append(f"{k}={v}") for target in build.targets: + if target.name.startswith("make:"): + continue cmd.append(target.name) return cmd diff --git a/tuxmake/target.py b/tuxmake/target.py index 2142357..e03da9d 100644 --- a/tuxmake/target.py +++ b/tuxmake/target.py @@ -1,5 +1,6 @@ from typing import List, Tuple +from configparser import ConfigParser from pathlib import Path import re import shlex @@ -279,6 +280,23 @@ def __init_config__(self): self.artifacts["vmlinux"] = "vmlinux" +class MakeTarget(Target): + """Pass-through target that runs `make `.""" + + def __init__(self, make_target, build, compression=default_compression): + self.build = build + self.compression = compression + self.target_arch = build.target_arch + self.name = f"make:{make_target}" + self.config = ConfigParser() + self.config["target"] = { + "description": f"make {make_target}", + "dependencies": "config", + "commands": shlex.quote("{make}") + " " + shlex.quote(make_target), + } + self.__init_config__() + + class Kselftest(Target): def __init_config__(self): super().__init_config__() @@ -303,6 +321,15 @@ def _is_clang_toolchain(self): } +_make_target_re = re.compile(r"/|\.(o|ko|s|i|lst|dtb|dtbo)$") + + def create_target(name, build, compression=default_compression): cls = __special_targets__.get(name, Target) + # Fall back to a pass-through make target when the name looks like + # a Kbuild target (contains / or ends in .o / .ko / .s / .i / ...). + # Lets users pass drivers/dma/ or kernel/livepatch/patch.o as + # positional arguments without --make-target=. + if cls is Target and _make_target_re.search(name): + return MakeTarget(name, build, compression) return cls(name, build, compression)