GNU Make Replacement

Make replacement

invoke write-all-the-programs run

You should end up with an executable under ws/ that exits with code 255.

tasks.py

Note the function testcompile, which is just a wrapper for compile that doesn’t have to know anything about the parameters it takes! That’s because of magicinvoke.get_params_from_ctx(), which was applied by magicinvoke.magictask().

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
import itertools
from textwrap import dedent

from magicinvoke import (
    magictask,
    get_params_from_ctx,
    InputPath,
    OutputPath,
    Lazy,
)

"""Yes, I'm aware that this should not be used as a build tool :)"""


@magictask(params_from="ctx.mycompileinfo", skippable=True)
def write_all_the_programs(
    ctx, cfiles, executable_cfile=Lazy("ctx.mycompileinfo.cfiles[0]")
):
    """First, write a .c file with a ``main`` and some other ones to be 'libraries'."""
    ctx.run("echo 'int main(void){return 255;}' > " + str(executable_cfile))
    ctx.run("touch " + " ".join(str(x) for x in cfiles))


@magictask(params_from="ctx.mycompileinfo", skippable=True)
def mycompile(ctx, cfiles, objectfiles: [OutputPath]):
    """Then compile them"""
    for c, o in zip(cfiles, objectfiles):
        ctx.run("gcc -c {} -o {}".format(c, o))


@magictask(params_from="ctx.mycompileinfo", pre=[mycompile], skippable=True)
def link(ctx, objectfiles: [InputPath], executable_path: OutputPath):
    """Now we link them into our final executable..."""
    ctx.run(
        "gcc -o {} {}".format(
            executable_path, " ".join(str(f) for f in objectfiles)
        )
    )


@magictask(params_from="ctx.mycompileinfo", pre=[link], skippable=True)
def run(ctx, executable_path: InputPath):
    """And finally run the executable, exiting with exitcode=255."""
    # Calling link(ctx) here would would work just as well as pre=[link], but
    #  rather than invoke checking if link needs to run _once_ when the application
    #  starts,  for all tasks that depend on it (see task-deduping page),
    #  we have to check if it needs to be run each time someone calls it.
    #  Given that checking file timestamps is pretty fast, this probably isn't a big deal.
    ctx.run("{}".format(executable_path))


@magictask(params_from="ctx.mycompileinfo")
def touch(ctx, cfiles):
    """
    This task provided so that you can poke the files yourself and see that we
    only run the tasks that are necessary :)
    """
    for f in cfiles:
        ctx.run("touch {}".format(f))


@magictask(params_from="ctx.mycompileinfo")
def clean(ctx, cfiles, objectfiles, executable_path):
    removing = " ".join(
        str(p) for p in itertools.chain(cfiles, objectfiles, [executable_path])
    )
    ctx.run("rm {}".format(removing), warn=True)


@magictask
def test(ctx):
    """Don't mind me; used by automated tests to ensure the example stays working!"""
    # Whole pipeline should run when c sources change.
    expected_stdout = dedent(
        """
        gcc -c ws/a.c -o ws/a.o
        gcc -c ws/b.c -o ws/b.o
        gcc -c ws/c.c -o ws/c.o
        gcc -o ws/produced_executable ws/a.o ws/b.o ws/c.o
        ws/produced_executable
        """
    )
    # Note how we don't have to pass all the defaults in from ``ctx`` here :)
    # The semantics of calling a task from Python now match the cmd-line semantics.
    clean(ctx)
    write_all_the_programs(ctx)

    res = ctx.run("invoke run", warn=True)
    assert expected_stdout.strip() == res.stdout.strip()

    # Test 2, Only last step should run if next to last step's output changed.
    expected_stdout = dedent(
        """
        gcc -o ws/produced_executable ws/a.o ws/b.o ws/c.o
        ws/produced_executable
        """
    )
    ctx.run("touch {}".format(ctx.mycompileinfo.objectfiles[0]))
    res = ctx.run("invoke run", warn=True)
    assert expected_stdout.strip() == res.stdout.strip()

    print("All tests succeeded.")

invoke.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pathlib import Path

# Setup a workspace when config is loaded because why not
prefix = Path("./ws")
prefix.mkdir(exist_ok=True)

# Hardcode our sources, but we could automagically detect them.
sources = ["a.c", "b.c", "c.c"]
sources = [prefix / s for s in sources]


def replace_suffix(replacing_list, with_what):
    return list(s.with_suffix(with_what) for s in replacing_list)


# Configure an dict that will supply defaults to all of our arguments
mycompileinfo = dict(
    cfiles=sources,
    objectfiles=[source.with_suffix(".o") for source in sources],
    executable_path=prefix / "produced_executable",
)

# Turn on echoing so that we can see it work
run = {"echo": True}