Repository: systemd/mkosi

Issue:
ExtraTree= pointing to a non-existent directory results in a backtrace
```console
$ ls mkosi.extra      
ls: cannot access 'mkosi.extra': No such file or directory

$ sudo mkosi build
‣  Copying in extra file trees…
‣  (Unmounting image)
‣  (Detaching /dev/loop0)
Traceback (most recent call last):
  File "/usr/lib64/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib64/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/usr/lib/python3.10/site-packages/mkosi/__main__.py", line 36, in <module>
    main()
  File "/usr/lib/python3.10/site-packages/mkosi/__main__.py", line 27, in main
    run_verb(a)
  File "/usr/lib/python3.10/site-packages/mkosi/__init__.py", line 8409, in run_verb
    manifest = build_stuff(args)
  File "/usr/lib/python3.10/site-packages/mkosi/__init__.py", line 7772, in build_stuff
    image = build_image(args, root, manifest=manifest, do_run_build_script=False, cleanup=True)
  File "/usr/lib/python3.10/site-packages/mkosi/__init__.py", line 7506, in build_image
    install_extra_trees(args, root, for_cache)
  File "/usr/lib/python3.10/site-packages/mkosi/__init__.py", line 3650, in install_extra_trees
    shutil.unpack_archive(cast(str, tree), root)
  File "/usr/lib64/python3.10/shutil.py", line 1302, in unpack_archive
    raise ReadError("Unknown archive format '{0}'".format(filename))
shutil.ReadError: Unknown archive format '/home/zbyszek/pythonschool/vm/mkosi/mkosi.extra'
```

I guess we should check for presence before creating the whole image.


Discussion:
https://github.com/python/cpython/issues/98758

I also see that mkosi doesn't print the summary to stdout, so grep doesn't work.

Resolving patch:
diff --git a/mkosi/__init__.py b/mkosi/__init__.py
--- a/mkosi/__init__.py
+++ b/mkosi/__init__.py
@@ -105,6 +105,8 @@
 from .syscall import blkpg_add_partition, blkpg_del_partition, reflink
 
 complete_step = MkosiPrinter.complete_step
+color_error = MkosiPrinter.color_error
+
 
 __version__ = "13"
 
@@ -6404,14 +6406,7 @@ def script_path(value: Optional[str]) -> Optional[Path]:
 def normalize_script(path: Optional[Path]) -> Optional[Path]:
     if not path or path is DISABLED:
         return None
-    path = Path(path).absolute()
-    if not path.exists():
-        die(f"{path} does not exist")
-    if not path.is_file():
-        die(f"{path} is not a file")
-    if not os.access(path, os.X_OK):
-        die(f"{path} is not executable")
-    return path
+    return Path(path).absolute()
 
 
 def load_args(args: argparse.Namespace) -> MkosiConfig:
@@ -6796,7 +6791,46 @@ def cache_image_path(config: MkosiConfig, is_final_image: bool) -> Optional[Path
         return Path(f"{config.output}.{suffix}")
 
 
-def check_output(config: MkosiConfig) -> None:
+def check_tree_input(path: Optional[Path]) -> None:
+    # Each path may be a directory or a tarball.
+    # Open the file or directory to simulate an access check.
+    # If that fails, an exception will be thrown.
+    if not path:
+        return
+
+    os.open(path, os.R_OK)
+
+
+def check_script_input(path: Optional[Path]) -> None:
+    if not path:
+        return
+
+    os.open(path, os.R_OK)
+    if not path.is_file():
+        raise OSError(errno.ENOENT, 'Not a normal file')
+    if not os.access(path, os.X_OK):
+        raise OSError(errno.ENOENT, 'Not executable')
+    return None
+
+
+def check_inputs(config: MkosiConfig) -> None:
+    try:
+        for tree in (config.skeleton_trees,
+                     config.extra_trees):
+            for item in tree:
+                check_tree_input(item)
+
+        for path in (config.build_script,
+                     config.prepare_script,
+                     config.postinst_script,
+                     config.finalize_script,
+                     config.base_image):
+            check_script_input(path)
+    except OSError as e:
+        die(f'{e.filename} {e.strerror}')
+
+
+def check_outputs(config: MkosiConfig) -> None:
     if config.skip_final_phase:
         return
 
@@ -6813,7 +6847,6 @@ def check_output(config: MkosiConfig) -> None:
         config.output_split_verity_sig if config.split_artifacts else None,
         config.output_split_kernel if config.split_artifacts else None,
     ):
-
         if f and f.exists():
             die(f"Output path {f} exists already. (Consider invocation with --force.)")
 
@@ -6843,115 +6876,133 @@ def format_bytes_or_auto(sz: int) -> str:
 def none_to_na(s: Optional[T]) -> Union[T, str]:
     return "n/a" if s is None else s
 
-
 def none_to_no(s: Optional[T]) -> Union[T, str]:
     return "no" if s is None else s
 
+def none_to_none(s: Optional[T]) -> Union[T, str]:
+    return "none" if s is None else s
 
-def none_to_none(o: Optional[object]) -> str:
-    return "none" if o is None else str(o)
 
+def path_or_none(
+        path: Optional[Path],
+        checker: Optional[Callable[[Optional[Path]], None]] = None,
+) -> Union[Optional[Path], str]:
+    try:
+        if checker:
+            checker(path)
+    except OSError as e:
+        return f'{color_error(path)} ({e.strerror})'
+    else:
+        return path
 
-def line_join_list(array: Sequence[PathString]) -> str:
+def line_join_list(
+        array: Sequence[PathString],
+        checker: Optional[Callable[[Optional[Path]], None]] = None,
+) -> str:
     if not array:
         return "none"
-    return "\n                            ".join(str(item) for item in array)
+
+    items = (str(path_or_none(cast(Path, item), checker=checker)) for item in array)
+    return "\n                            ".join(items)
 
 
 def print_summary(config: MkosiConfig) -> None:
-    # FIXME: normal print
-    MkosiPrinter.info("COMMANDS:")
-    MkosiPrinter.info(f"                      verb: {config.verb}")
-    MkosiPrinter.info("                   cmdline: " + " ".join(config.cmdline))
-    MkosiPrinter.info("\nDISTRIBUTION:")
-    MkosiPrinter.info("              Distribution: " + config.distribution.name)
-    MkosiPrinter.info("                   Release: " + none_to_na(config.release))
-    MkosiPrinter.info("              Architecture: " + config.architecture)
+    print("COMMANDS:")
+
+    print("                      verb:", config.verb)
+    print("                   cmdline:", " ".join(config.cmdline))
+
+    print("\nDISTRIBUTION:")
+
+    print("              Distribution:", config.distribution.name)
+    print("                   Release:", none_to_na(config.release))
+    print("              Architecture:", config.architecture)
+
     if config.mirror is not None:
-        MkosiPrinter.info("                    Mirror: " + config.mirror)
+        print("                    Mirror:", config.mirror)
+
     if config.local_mirror is not None:
-        MkosiPrinter.info("      Local Mirror (build): " + config.local_mirror)
-    MkosiPrinter.info(f"  Repo Signature/Key check: {yes_no(config.repository_key_check)}")
+        print("      Local Mirror (build):", config.local_mirror)
+
+    print("  Repo Signature/Key check:", yes_no(config.repository_key_check))
+
     if config.repositories is not None and len(config.repositories) > 0:
-        MkosiPrinter.info("              Repositories: " + ",".join(config.repositories))
-    MkosiPrinter.info("     Use Host Repositories: " + yes_no(config.use_host_repositories))
-    MkosiPrinter.info("\nOUTPUT:")
+        print("              Repositories:", ",".join(config.repositories))
+
+    print("     Use Host Repositories:", yes_no(config.use_host_repositories))
+
+    print("\nOUTPUT:")
+
     if config.hostname:
-        MkosiPrinter.info("                  Hostname: " + config.hostname)
+        print("                  Hostname:", config.hostname)
+
     if config.image_id is not None:
-        MkosiPrinter.info("                  Image ID: " + config.image_id)
+        print("                  Image ID:", config.image_id)
+
     if config.image_version is not None:
-        MkosiPrinter.info("             Image Version: " + config.image_version)
-    MkosiPrinter.info("             Output Format: " + config.output_format.name)
+        print("             Image Version:", config.image_version)
+
+    print("             Output Format:", config.output_format.name)
+
     maniformats = (" ".join(str(i) for i in config.manifest_format)) or "(none)"
-    MkosiPrinter.info("          Manifest Formats: " + maniformats)
+    print("          Manifest Formats:", maniformats)
+
     if config.output_format.can_minimize():
-        MkosiPrinter.info("                  Minimize: " + yes_no(config.minimize))
+        print("                  Minimize:", yes_no(config.minimize))
+
     if config.output_dir:
-        MkosiPrinter.info(f"          Output Directory: {config.output_dir}")
+        print("          Output Directory:", config.output_dir)
+
     if config.workspace_dir:
-        MkosiPrinter.info(f"       Workspace Directory: {config.workspace_dir}")
-    MkosiPrinter.info(f"                    Output: {config.output}")
-    MkosiPrinter.info(f"           Output Checksum: {none_to_na(config.output_checksum if config.checksum else None)}")
-    MkosiPrinter.info(f"          Output Signature: {none_to_na(config.output_signature if config.sign else None)}")
-    MkosiPrinter.info(f"               Output Bmap: {none_to_na(config.output_bmap if config.bmap else None)}")
-    MkosiPrinter.info(f"  Generate split artifacts: {yes_no(config.split_artifacts)}")
-    MkosiPrinter.info(
-        f"      Output Split Root FS: {none_to_na(config.output_split_root if config.split_artifacts else None)}"
-    )
-    MkosiPrinter.info(
-        f"       Output Split Verity: {none_to_na(config.output_split_verity if config.split_artifacts else None)}"
-    )
-    MkosiPrinter.info(
-        f"  Output Split Verity Sig.: {none_to_na(config.output_split_verity_sig if config.split_artifacts else None)}"
-    )
-    MkosiPrinter.info(
-        f"       Output Split Kernel: {none_to_na(config.output_split_kernel if config.split_artifacts else None)}"
-    )
-    MkosiPrinter.info(
-        f"    Output nspawn Settings: {none_to_na(config.output_nspawn_settings if config.nspawn_settings is not None else None)}"
-    )
-    MkosiPrinter.info(
-        f"                   SSH key: {none_to_na((config.ssh_key or config.output_sshkey or config.ssh_agent) if config.ssh else None)}"
-    )
+        print("       Workspace Directory:", config.workspace_dir)
+
+    print("                    Output:", config.output)
+    print("           Output Checksum:", none_to_na(config.output_checksum if config.checksum else None))
+    print("          Output Signature:", none_to_na(config.output_signature if config.sign else None))
+    print("               Output Bmap:", none_to_na(config.output_bmap if config.bmap else None))
+    print("  Generate split artifacts:", yes_no(config.split_artifacts))
+    print("      Output Split Root FS:", none_to_na(config.output_split_root if config.split_artifacts else None))
+    print("       Output Split Verity:", none_to_na(config.output_split_verity if config.split_artifacts else None))
+    print("  Output Split Verity Sig.:", none_to_na(config.output_split_verity_sig if config.split_artifacts else None))
+    print("       Output Split Kernel:", none_to_na(config.output_split_kernel if config.split_artifacts else None))
+    print("    Output nspawn Settings:", none_to_na(config.output_nspawn_settings if config.nspawn_settings is not None else None))
+    print("                   SSH key:", none_to_na((config.ssh_key or config.output_sshkey or config.ssh_agent) if config.ssh else None))
     if config.ssh_port != 22:
-        MkosiPrinter.info(f"                  SSH port: {config.ssh_port}")
+        print("                  SSH port:", config.ssh_port)
 
-    MkosiPrinter.info("               Incremental: " + yes_no(config.incremental))
-
-    MkosiPrinter.info("                 Read-only: " + yes_no(config.read_only))
-
-    MkosiPrinter.info(" Internal (FS) Compression: " + yes_no_or(should_compress_fs(config)))
-    MkosiPrinter.info("Outer (output) Compression: " + yes_no_or(should_compress_output(config)))
+    print("               Incremental:", yes_no(config.incremental))
+    print("                 Read-only:", yes_no(config.read_only))
+    print(" Internal (FS) Compression:", yes_no_or(should_compress_fs(config)))
+    print("Outer (output) Compression:", yes_no_or(should_compress_output(config)))
 
     if config.mksquashfs_tool:
-        MkosiPrinter.info("           Mksquashfs tool: " + " ".join(map(str, config.mksquashfs_tool)))
+        print("           Mksquashfs tool:", " ".join(map(str, config.mksquashfs_tool)))
 
     if config.output_format.is_disk():
-        MkosiPrinter.info("                     QCow2: " + yes_no(config.qcow2))
+        print("                     QCow2:", yes_no(config.qcow2))
 
-    MkosiPrinter.info("                Encryption: " + none_to_no(config.encrypt))
-    MkosiPrinter.info("                    Verity: " + yes_no_or(config.verity))
+    print("                Encryption:", none_to_no(config.encrypt))
+    print("                    Verity:", yes_no_or(config.verity))
 
     if config.output_format.is_disk():
-        MkosiPrinter.info("                  Bootable: " + yes_no(config.bootable))
+        print("                  Bootable:", yes_no(config.bootable))
 
         if config.bootable:
-            MkosiPrinter.info("       Kernel Command Line: " + " ".join(config.kernel_command_line))
-            MkosiPrinter.info("           UEFI SecureBoot: " + yes_no(config.secure_boot))
-
-            MkosiPrinter.info("     Unified Kernel Images: " + yes_no(config.with_unified_kernel_images))
-            MkosiPrinter.info("             GPT First LBA: " + str(config.gpt_first_lba))
-            MkosiPrinter.info("           Hostonly Initrd: " + yes_no(config.hostonly_initrd))
+            print("       Kernel Command Line:", " ".join(config.kernel_command_line))
+            print("           UEFI SecureBoot:", yes_no(config.secure_boot))
+            print("     Unified Kernel Images:", yes_no(config.with_unified_kernel_images))
+            print("             GPT First LBA:", str(config.gpt_first_lba))
+            print("           Hostonly Initrd:", yes_no(config.hostonly_initrd))
 
     if config.secure_boot or config.verity == "sign":
-        MkosiPrinter.info(f"SecureBoot/Verity Sign Key: {config.secure_boot_key}")
-        MkosiPrinter.info(f"   SecureBoot/verity Cert.: {config.secure_boot_certificate}")
+        print("SecureBoot/Verity Sign Key:", config.secure_boot_key)
+        print("   SecureBoot/verity Cert.:", config.secure_boot_certificate)
 
-    MkosiPrinter.info("                Machine ID: " + none_to_no(config.machine_id))
+    print("                Machine ID:", none_to_no(config.machine_id))
 
-    MkosiPrinter.info("\nCONTENT:")
-    MkosiPrinter.info("                  Packages: " + line_join_list(config.packages))
+    print("\nCONTENT:")
+
+    print("                  Packages:", line_join_list(config.packages))
 
     if config.distribution in (
         Distribution.fedora,
@@ -6963,63 +7014,70 @@ def print_summary(config: MkosiConfig) -> None:
         Distribution.alma,
         Distribution.alma_epel,
     ):
-        MkosiPrinter.info("        With Documentation: " + yes_no(config.with_docs))
+        print("        With Documentation:", yes_no(config.with_docs))
+
+    print("             Package Cache:", none_to_none(config.cache_path))
+    print("               Extra Trees:", line_join_list(config.extra_trees, check_tree_input))
+    print("            Skeleton Trees:", line_join_list(config.skeleton_trees, check_tree_input))
+    print("      CleanPackageMetadata:", yes_no_or(config.clean_package_metadata))
 
-    MkosiPrinter.info("             Package Cache: " + none_to_none(config.cache_path))
-    MkosiPrinter.info("               Extra Trees: " + line_join_list(config.extra_trees))
-    MkosiPrinter.info("            Skeleton Trees: " + line_join_list(config.skeleton_trees))
-    MkosiPrinter.info("      CleanPackageMetadata: " + yes_no_or(config.clean_package_metadata))
     if config.remove_files:
-        MkosiPrinter.info("              Remove Files: " + line_join_list(config.remove_files))
+        print("              Remove Files:", line_join_list(config.remove_files))
     if config.remove_packages:
-        MkosiPrinter.info("           Remove Packages: " + line_join_list(config.remove_packages))
-    MkosiPrinter.info("              Build Script: " + none_to_none(config.build_script))
-    env = [f"{k}={v}" for k, v in config.environment.items()]
-    MkosiPrinter.info("        Script Environment: " + line_join_list(env))
+        print("           Remove Packages:", line_join_list(config.remove_packages))
+
+    print("             Build Sources:", none_to_none(config.build_sources))
+    print("      Source File Transfer:", none_to_none(config.source_file_transfer))
+    print("Source File Transfer Final:", none_to_none(config.source_file_transfer_final))
+    print("           Build Directory:", none_to_none(config.build_dir))
+    print("         Include Directory:", none_to_none(config.include_dir))
+    print("         Install Directory:", none_to_none(config.install_dir))
+    print("            Build Packages:", line_join_list(config.build_packages))
+    print("          Skip final phase:", yes_no(config.skip_final_phase))
 
+    print("              Build Script:", path_or_none(config.build_script, check_script_input))
+
+    env = [f"{k}={v}" for k, v in config.environment.items()]
     if config.build_script:
-        MkosiPrinter.info("                 Run tests: " + yes_no(config.with_tests))
-
-    MkosiPrinter.info("                  Password: " + ("default" if config.password is None else "set"))
-    MkosiPrinter.info("                 Autologin: " + yes_no(config.autologin))
-
-    MkosiPrinter.info("             Build Sources: " + none_to_none(config.build_sources))
-    MkosiPrinter.info("      Source File Transfer: " + none_to_none(config.source_file_transfer))
-    MkosiPrinter.info("Source File Transfer Final: " + none_to_none(config.source_file_transfer_final))
-    MkosiPrinter.info("           Build Directory: " + none_to_none(config.build_dir))
-    MkosiPrinter.info("         Include Directory: " + none_to_none(config.include_dir))
-    MkosiPrinter.info("         Install Directory: " + none_to_none(config.install_dir))
-    MkosiPrinter.info("            Build Packages: " + line_join_list(config.build_packages))
-    MkosiPrinter.info("          Skip final phase: " + yes_no(config.skip_final_phase))
-    MkosiPrinter.info("        Postinstall Script: " + none_to_none(config.postinst_script))
-    MkosiPrinter.info("            Prepare Script: " + none_to_none(config.prepare_script))
-    MkosiPrinter.info("           Finalize Script: " + none_to_none(config.finalize_script))
-    MkosiPrinter.info("      Scripts with network: " + yes_no_or(config.with_network))
-    MkosiPrinter.info("           nspawn Settings: " + none_to_none(config.nspawn_settings))
+        print("                 Run tests:", yes_no(config.with_tests))
+
+    print("        Postinstall Script:", path_or_none(config.postinst_script, check_script_input))
+    print("            Prepare Script:", path_or_none(config.prepare_script, check_script_input))
+    print("           Finalize Script:", path_or_none(config.finalize_script, check_script_input))
+
+    print("        Script Environment:", line_join_list(env))
+    print("      Scripts with network:", yes_no_or(config.with_network))
+    print("           nspawn Settings:", none_to_none(config.nspawn_settings))
+
+    print("                  Password:", ("default" if config.password is None else "set"))
+    print("                 Autologin:", yes_no(config.autologin))
 
     if config.output_format.is_disk():
-        MkosiPrinter.info("\nPARTITIONS:")
-        MkosiPrinter.info("            Root Partition: " + format_bytes_or_auto(config.root_size))
-        MkosiPrinter.info("            Swap Partition: " + format_bytes_or_disabled(config.swap_size))
-        MkosiPrinter.info("             EFI Partition: " + format_bytes_or_disabled(config.esp_size))
-        MkosiPrinter.info("        XBOOTLDR Partition: " + format_bytes_or_disabled(config.xbootldr_size))
-        MkosiPrinter.info("           /home Partition: " + format_bytes_or_disabled(config.home_size))
-        MkosiPrinter.info("            /srv Partition: " + format_bytes_or_disabled(config.srv_size))
-        MkosiPrinter.info("            /var Partition: " + format_bytes_or_disabled(config.var_size))
-        MkosiPrinter.info("        /var/tmp Partition: " + format_bytes_or_disabled(config.tmp_size))
-        MkosiPrinter.info("            BIOS Partition: " + format_bytes_or_disabled(config.bios_size))
-        MkosiPrinter.info("                 /usr only: " + yes_no(config.usr_only))
-
-        MkosiPrinter.info("\nVALIDATION:")
-        MkosiPrinter.info("                  Checksum: " + yes_no(config.checksum))
-        MkosiPrinter.info("                      Sign: " + yes_no(config.sign))
-        MkosiPrinter.info("                   GPG Key: " + ("default" if config.key is None else config.key))
-
-    MkosiPrinter.info("\nHOST CONFIGURATION:")
-    MkosiPrinter.info("        Extra search paths: " + line_join_list(config.extra_search_paths))
-    MkosiPrinter.info("             QEMU Headless: " + yes_no(config.qemu_headless))
-    MkosiPrinter.info("      QEMU Extra Arguments: " + line_join_list(config.qemu_args))
-    MkosiPrinter.info("                    Netdev: " + yes_no(config.netdev))
+        print("\nPARTITIONS:")
+
+        print("            Root Partition:", format_bytes_or_auto(config.root_size))
+        print("            Swap Partition:", format_bytes_or_disabled(config.swap_size))
+        print("             EFI Partition:", format_bytes_or_disabled(config.esp_size))
+        print("        XBOOTLDR Partition:", format_bytes_or_disabled(config.xbootldr_size))
+        print("           /home Partition:", format_bytes_or_disabled(config.home_size))
+        print("            /srv Partition:", format_bytes_or_disabled(config.srv_size))
+        print("            /var Partition:", format_bytes_or_disabled(config.var_size))
+        print("        /var/tmp Partition:", format_bytes_or_disabled(config.tmp_size))
+        print("            BIOS Partition:", format_bytes_or_disabled(config.bios_size))
+        print("                 /usr only:", yes_no(config.usr_only))
+
+        print("\nVALIDATION:")
+
+        print("                  Checksum:", yes_no(config.checksum))
+        print("                      Sign:", yes_no(config.sign))
+        print("                   GPG Key:", ("default" if config.key is None else config.key))
+
+    print("\nHOST CONFIGURATION:")
+
+    print("        Extra search paths:", line_join_list(config.extra_search_paths))
+    print("             QEMU Headless:", yes_no(config.qemu_headless))
+    print("      QEMU Extra Arguments:", line_join_list(config.qemu_args))
+    print("                    Netdev:", yes_no(config.netdev))
 
 
 def reuse_cache_tree(state: MkosiState, cached: bool) -> bool:
@@ -8238,8 +8296,11 @@ def run_verb(raw: argparse.Namespace) -> None:
         if config.verb in MKOSI_COMMANDS_SUDO:
             check_root()
 
-        if config.verb == Verb.build and not config.force:
-            check_output(config)
+        if config.verb == Verb.build:
+            check_inputs(config)
+
+            if not config.force:
+                check_outputs(config)
 
         if needs_build(config) or config.verb == Verb.clean:
             check_root()
diff --git a/mkosi/backend.py b/mkosi/backend.py
--- a/mkosi/backend.py
+++ b/mkosi/backend.py
@@ -914,6 +914,10 @@ class MkosiPrinter:
     def _print(cls, text: str) -> None:
         cls.out_file.write(text)
 
+    @classmethod
+    def color_error(cls, text: Any) -> str:
+        return f"{cls.red}{text}{cls.reset}"
+
     @classmethod
     def print_step(cls, text: str) -> None:
         prefix = cls.prefix + " " * cls.level
@@ -932,7 +936,7 @@ def info(cls, text: str) -> None:
 
     @classmethod
     def warn(cls, text: str) -> None:
-        cls._print(f"{cls.prefix}{cls.red}{text}{cls.reset}\n")
+        cls._print(f"{cls.prefix}{cls.color_error(text)}\n")
 
     @classmethod
     @contextlib.contextmanager
diff --git a/mkosi/machine.py b/mkosi/machine.py
--- a/mkosi/machine.py
+++ b/mkosi/machine.py
@@ -18,8 +18,9 @@
     MKOSI_COMMANDS_NEED_BUILD,
     CompletedProcess,
     build_stuff,
+    check_inputs,
     check_native,
-    check_output,
+    check_outputs,
     check_root,
     init_namespace,
     load_args,
@@ -113,7 +114,8 @@ def build(self) -> None:
             unlink_output(self.config)
 
         if self.config.verb == Verb.build:
-            check_output(self.config)
+            check_inputs(self.config)
+            check_outputs(self.config)
 
         if needs_build(self.config):
             check_root()


Test patch: