Repository: systemd/mkosi

Issue:
Add an option to keep workspace directory when an error occurs
I am currently running into issues with GPG signing, and am unable to debug them because the workspace directory is cleared automatically after exit. The addition of a debug option to assist would be helpful.


Discussion:

Resolving patch:
diff --git a/mkosi/__init__.py b/mkosi/__init__.py
--- a/mkosi/__init__.py
+++ b/mkosi/__init__.py
@@ -44,7 +44,7 @@
 from mkosi.distributions import Distribution
 from mkosi.installer import clean_package_manager_metadata, package_manager_scripts
 from mkosi.kmod import gen_required_kernel_modules, process_kernel_modules
-from mkosi.log import ARG_DEBUG, complete_step, die, log_step
+from mkosi.log import ARG_DEBUG, complete_step, die, log_notice, log_step
 from mkosi.manifest import Manifest
 from mkosi.mounts import mount, mount_overlay, mount_passwd, mount_usr
 from mkosi.pager import page
@@ -2113,12 +2113,27 @@ def normalize_mtime(root: Path, mtime: Optional[int], directory: Optional[Path]
             os.utime(p, (mtime, mtime), follow_symlinks=False)
 
 
+@contextlib.contextmanager
+def setup_workspace(args: MkosiArgs, config: MkosiConfig) -> Iterator[Path]:
+    with contextlib.ExitStack() as stack:
+        workspace = Path(tempfile.mkdtemp(dir=config.workspace_dir_or_default(), prefix="mkosi-workspace"))
+        stack.callback(lambda: rmtree(workspace))
+
+        with scopedenv({"TMPDIR" : os.fspath(workspace)}):
+            try:
+                yield Path(workspace)
+            except BaseException:
+                if args.debug_workspace:
+                    stack.pop_all()
+                    log_notice(f"Workspace: {workspace}")
+                    workspace.chmod(0o755)
+
+
 def build_image(args: MkosiArgs, config: MkosiConfig) -> None:
     manifest = Manifest(config) if config.manifest_format else None
-    workspace = tempfile.TemporaryDirectory(dir=config.workspace_dir_or_default(), prefix=".mkosi-tmp")
 
-    with workspace, scopedenv({"TMPDIR" : workspace.name}):
-        state = MkosiState(args, config, Path(workspace.name))
+    with setup_workspace(args, config) as workspace:
+        state = MkosiState(args, config, workspace)
         install_package_manager_trees(state)
 
         with mount_base_trees(state):
diff --git a/mkosi/config.py b/mkosi/config.py
--- a/mkosi/config.py
+++ b/mkosi/config.py
@@ -648,6 +648,7 @@ class MkosiArgs:
     directory: Optional[Path]
     debug: bool
     debug_shell: bool
+    debug_workspace: bool
     pager: bool
     genkey_valid_days: str
     genkey_common_name: str
@@ -2020,6 +2021,12 @@ def create_argument_parser(action: type[argparse.Action]) -> argparse.ArgumentPa
         action="store_true",
         default=False,
     )
+    parser.add_argument(
+        "--debug-workspace",
+        help="When an error occurs, the workspace directory will not be deleted",
+        action="store_true",
+        default=False,
+    )
     parser.add_argument(
         "--no-pager",
         action="store_false",
@@ -2772,8 +2779,8 @@ def bold(s: Any) -> str:
               Manifest Formats: {maniformats}
                         Output: {bold(config.output_with_compression)}
                    Compression: {config.compress_output}
-              Output Directory: {none_to_default(config.output_dir)}
-           Workspace Directory: {none_to_default(config.workspace_dir)}
+              Output Directory: {config.output_dir_or_cwd()}
+           Workspace Directory: {config.workspace_dir_or_default()}
                Cache Directory: {none_to_none(config.cache_dir)}
                Build Directory: {none_to_none(config.build_dir)}
                       Image ID: {config.image_id}
diff --git a/mkosi/log.py b/mkosi/log.py
--- a/mkosi/log.py
+++ b/mkosi/log.py
@@ -44,6 +44,10 @@ def log_step(text: str) -> None:
         logging.info(f"{prefix}{Style.bold}{text}{Style.reset}")
 
 
+def log_notice(text: str) -> None:
+    logging.info(f"{Style.bold}{text}{Style.reset}")
+
+
 @contextlib.contextmanager
 def complete_step(text: str, text2: Optional[str] = None) -> Iterator[list[Any]]:
     global LEVEL


Test patch:
diff --git a/tests/test_json.py b/tests/test_json.py
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -39,6 +39,7 @@ def test_args(path: Optional[Path]) -> None:
             ],
             "Debug": false,
             "DebugShell": false,
+            "DebugWorkspace": false,
             "Directory": {f'"{os.fspath(path)}"' if path is not None else 'null'},
             "DocFormat": "auto",
             "Force": 9001,
@@ -56,6 +57,7 @@ def test_args(path: Optional[Path]) -> None:
         cmdline = ["foo", "bar"],
         debug = False,
         debug_shell = False,
+        debug_workspace = False,
         directory = Path(path) if path is not None else None,
         doc_format = DocFormat.auto,
         force = 9001,