Skip to content

cli

CLI application definition for junix.

Build

Bases: Application

Build flake attributes and produce JUnit XML.

Source code in src/junix/cli.py
@Junix.subcommand("build")
class Build(cli.Application):  # noqa: D415
    """Build flake attributes and produce JUnit XML."""

    PROGNAME = "junix build"

    output = cli.SwitchAttr(
        ["-o", "--output"],
        help="Path to write the JUnit XML report (default: stdout).",
    )
    store = cli.SwitchAttr(
        ["-s", "--store"],
        help="Remote store URL (e.g. ssh-ng://eu.nixbuild.net).",
    )

    @_plumbum_async_main
    async def main(self, *attrs: str) -> int:  # noqa: D102
        self.parent.log(f"building: {' '.join(attrs)}", level=2)
        xml, exit_code = await run_nix_build(
            list(attrs),
            suite_name="nix-build",
            store=self.store,
            log_level=self.parent._nix_log_level,
        )
        _write_report(xml, self.output)
        return exit_code

Check

Bases: Application

Evaluate and build all checks of a flake.

Source code in src/junix/cli.py
@Junix.subcommand("check")
class Check(cli.Application):  # noqa: D415
    """Evaluate and build all checks of a flake."""

    PROGNAME = "junix check"

    output = cli.SwitchAttr(
        ["-o", "--output"],
        help="Path to write the JUnit XML report (default: stdout).",
    )
    build_arch = cli.SwitchAttr(
        ["--build-arch"],
        list=True,
        help="Architecture to build for (default: local; repeatable).",
    )
    eval_arch = cli.SwitchAttr(
        ["--eval-arch"],
        list=True,
        help="Architecture to evaluate (default: all; repeatable).",
    )

    @_plumbum_async_main
    async def main(self, path: str = ".") -> int:  # noqa: D102
        self.parent.log("discovering checks ...", level=1)

        check_attrs = await discover_checks(path, eval_arch=self.eval_arch)

        if not check_attrs:
            self.parent.log("no checks found", level=1)
            _write_report(report_to_xml([], suite_name="nix-check"), self.output)
            return 0

        self.parent.log(f"found {len(check_attrs)} check(s)", level=1)

        cmd = list(check_attrs)
        if self.build_arch:
            for arch in self.build_arch:
                cmd = ["--system", arch] + cmd

        xml, exit_code = await run_nix_build(
            cmd,
            suite_name="nix-check",
            default_name="nix check",
            log_level=self.parent._nix_log_level,
            expected_count=len(check_attrs),
        )
        _write_report(xml, self.output)
        return exit_code

Junix

Bases: Application

Turn Nix build logs into JUnit XML reports for your CI.

Source code in src/junix/cli.py
class Junix(cli.Application):
    """Turn Nix build logs into JUnit XML reports for your CI."""

    PROGNAME = "junix"
    VERSION = __version__

    _verbose: int = 0

    @cli.switch(
        ["-v", "--verbose"],
        list=True,
        help="Increase log verbosity (repeat for more detail).",
    )
    def verbose(self, times: list[str]) -> None:  # type: ignore[misc]  # noqa: D102
        self._verbose = len(times)

    @property
    def _nix_log_level(self) -> int:
        """Map CLI verbosity to Nix log level threshold.

        Nix log levels:

        ```
        0 = lvlError
        1 = lvlWarn
        2 = lvlNotice
        3 = lvlInfo
        4 = lvlTalkative
        5 = lvlChatty
        6 = lvlDebug
        7 = lvlVomit
        ```

        Returns:
            Nix log level threshold (default 0 = errors only).
        """
        return {0: 0, 1: 3, 2: 5, 3: 7}.get(self._verbose, 7)

    def log(self, message: str, level: int = 1) -> None:
        """Print a log message to stderr if verbosity is high enough.

        Args:
            message: The message to print.
            level: Minimum verbosity level required to print (default 1).
        """
        if self._verbose >= level:
            print(f"junix: {message}", file=sys.stderr)

_nix_log_level property

Map CLI verbosity to Nix log level threshold.

Nix log levels:

0 = lvlError
1 = lvlWarn
2 = lvlNotice
3 = lvlInfo
4 = lvlTalkative
5 = lvlChatty
6 = lvlDebug
7 = lvlVomit

Returns:

Type Description
int

Nix log level threshold (default 0 = errors only).

log(message, level=1)

Print a log message to stderr if verbosity is high enough.

Parameters:

Name Type Description Default
message str

The message to print.

required
level int

Minimum verbosity level required to print (default 1).

1
Source code in src/junix/cli.py
def log(self, message: str, level: int = 1) -> None:
    """Print a log message to stderr if verbosity is high enough.

    Args:
        message: The message to print.
        level: Minimum verbosity level required to print (default 1).
    """
    if self._verbose >= level:
        print(f"junix: {message}", file=sys.stderr)

Translate

Bases: Application

Read @nix JSON lines from stdin and produce JUnit XML.

Source code in src/junix/cli.py
@Junix.subcommand("translate")
class Translate(cli.Application):  # noqa: D415
    """Read `@nix` JSON lines from stdin and produce JUnit XML."""

    PROGNAME = "junix translate"

    output = cli.SwitchAttr(
        ["-o", "--output"],
        help="Path to write the JUnit XML report (default: stdout).",
    )

    def main(self) -> int:  # noqa: D102
        xml, exit_code = translate_stream(sys.stdin, suite_name="nix-translate")
        _write_report(xml, self.output)
        return exit_code

_plumbum_async_main(main_method)

Wrap an async main() so plumbum can call it synchronously.

Plumbum 2's Application.run calls inst.main() synchronously. This decorator bridges that gap by running the coroutine inside asyncio.run().

Important: does not use functools.wraps because plumbum 2 introspects self.main with inspect.getfullargspec() and get_type_hints(). wraps would copy the original's annotations but the wrapper has args, *kwargs, causing a mismatch.

Parameters:

Name Type Description Default
main_method Callable[..., Awaitable[int]]

The async main() coroutine function to wrap.

required

Returns:

Type Description
Callable[..., int]

A sync wrapper function that runs the coroutine via asyncio.run().

Source code in src/junix/cli.py
def _plumbum_async_main(
    main_method: Callable[..., Awaitable[int]],
) -> Callable[..., int]:
    """Wrap an async main() so plumbum can call it synchronously.

    Plumbum 2's Application.run calls inst.main() synchronously. This
    decorator bridges that gap by running the coroutine inside
    asyncio.run().

    Important: does not use functools.wraps because plumbum 2
    introspects self.main with inspect.getfullargspec() and
    get_type_hints(). wraps would copy the original's annotations but
    the wrapper has *args, **kwargs, causing a mismatch.

    Args:
        main_method: The async main() coroutine function to wrap.

    Returns:
        A sync wrapper function that runs the coroutine via asyncio.run().
    """

    def wrapper(*args, **kwargs):
        return asyncio.run(main_method(*args, **kwargs))

    return wrapper

main()

Run the CLI and return the exit code.

Source code in src/junix/cli.py
def main() -> int:
    """Run the CLI and return the exit code."""
    return Junix.run()[1]