Repository: systemd/mkosi

Issue:
Add ToolsTreeRepositores / ToolsTreePackageManagerTrees
For an image I'm building, I want to include the Asahi Mesa library in the ToolsTree so that I can run QEMU images with GL acceleration.

However there's currently no way to add extra repos to the ToolsTree.

I locally have the below hack which passes PackageManagerTrees to the ToolsTree build also:
```
diff --git a/mkosi/__init__.py b/mkosi/__init__.py
index 74e20fe3..3800fd16 100644
--- a/mkosi/__init__.py
+++ b/mkosi/__init__.py
@@ -3593,6 +3593,7 @@ def finalize_default_tools(args: Args, config: Config, *, resources: Path) -> Co
         *(["--package-cache-dir", str(config.package_cache_dir)] if config.package_cache_dir else []),
         "--incremental", str(config.incremental),
         "--acl", str(config.acl),
+        *([f"--package-manager-tree={package.source}:{package.target}" for package in config.package_manager_trees]),
         *([f"--package={package}" for package in config.tools_tree_packages]),
         "--output", f"{config.tools_tree_distribution}-tools",
         *(["--source-date-epoch", str(config.source_date_epoch)] if config.source_date_epoch is not None else []),
```


Discussion:
Maybe mkosi could allow for custom tools tree similar to initrds. Ship definitions for one which works for most and otherwise make dependencies/images for work them
> Maybe mkosi could allow for custom tools tree similar to initrds. Ship definitions for one which works for most and otherwise make dependencies/images for work them

Maybe read the documentation for the ToolsTree option ;)
Fair enough :D I forgot that it worked with `Dependencies=`/images

Resolving patch:
diff --git a/mkosi/__init__.py b/mkosi/__init__.py
--- a/mkosi/__init__.py
+++ b/mkosi/__init__.py
@@ -40,7 +40,6 @@
     Verb,
     __version__,
     format_bytes,
-    format_tree,
     parse_config,
     summary,
     systemd_tool_version,
@@ -1577,7 +1576,7 @@ def build_default_initrd(context: Context) -> Path:
         *(["--mirror", context.config.mirror] if context.config.mirror else []),
         "--repository-key-check", str(context.config.repository_key_check),
         "--repositories", ",".join(context.config.repositories),
-        "--package-manager-tree", ",".join(format_tree(t) for t in context.config.package_manager_trees),
+        "--package-manager-tree", ",".join(str(t) for t in context.config.package_manager_trees),
         # Note that when compress_output == Compression.none == 0 we don't pass --compress-output which means the
         # default compression will get picked. This is exactly what we want so that initrds are always compressed.
         *(["--compress-output", str(context.config.compress_output)] if context.config.compress_output else []),
@@ -3585,6 +3584,8 @@ def finalize_default_tools(args: Args, config: Config, *, resources: Path) -> Co
         "--distribution", str(config.tools_tree_distribution),
         *(["--release", config.tools_tree_release] if config.tools_tree_release else []),
         *(["--mirror", config.tools_tree_mirror] if config.tools_tree_mirror else []),
+        "--repositories", ",".join(config.tools_tree_repositories),
+        "--package-manager-tree", ",".join(str(t) for t in config.tools_tree_package_manager_trees),
         "--repository-key-check", str(config.repository_key_check),
         "--cache-only", str(config.cacheonly),
         *(["--output-dir", str(config.output_dir)] if config.output_dir else []),
diff --git a/mkosi/config.py b/mkosi/config.py
--- a/mkosi/config.py
+++ b/mkosi/config.py
@@ -124,6 +124,9 @@ class ConfigTree:
     def with_prefix(self, prefix: Path = Path("/")) -> tuple[Path, Path]:
         return (self.source, prefix / os.fspath(self.target).lstrip("/") if self.target else prefix)
 
+    def __str__(self) -> str:
+        return f"{self.source}:{self.target}" if self.target else f"{self.source}"
+
 
 @dataclasses.dataclass(frozen=True)
 class QemuDrive:
@@ -1302,6 +1305,8 @@ class Config:
     tools_tree_distribution: Optional[Distribution]
     tools_tree_release: Optional[str]
     tools_tree_mirror: Optional[str]
+    tools_tree_repositories: list[str]
+    tools_tree_package_manager_trees: list[ConfigTree]
     tools_tree_packages: list[str]
     runtime_trees: list[ConfigTree]
     runtime_size: Optional[int]
@@ -2466,6 +2471,22 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
         default_factory=lambda ns: ns.mirror if ns.mirror and ns.distribution == ns.tools_tree_distribution else None,
         help="Set the mirror to use for the default tools tree",
     ),
+    ConfigSetting(
+        dest="tools_tree_repositories",
+        long="--tools-tree-repository",
+        metavar="REPOS",
+        section="Host",
+        parse=config_make_list_parser(delimiter=","),
+        help="Repositories to use for the default tools tree",
+    ),
+    ConfigSetting(
+        dest="tools_tree_package_manager_trees",
+        long="--tools-tree-package-manager-tree",
+        metavar="PATH",
+        section="Host",
+        parse=config_make_list_parser(delimiter=",", parse=make_tree_parser()),
+        help="Package manager trees for the default tools tree",
+    ),
     ConfigSetting(
         dest="tools_tree_packages",
         long="--tools-tree-package",
@@ -3431,24 +3452,8 @@ def none_to_default(s: Optional[object]) -> str:
     return "default" if s is None else str(s)
 
 
-def line_join_list(array: Iterable[PathString]) -> str:
-    if not array:
-        return "none"
-
-    items = (str(none_to_none(cast(Path, item))) for item in array)
-    return "\n                                     ".join(items)
-
-
-def format_tree(tree: ConfigTree) -> str:
-    return f"{tree.source}:{tree.target}" if tree.target else f"{tree.source}"
-
-
-def line_join_tree_list(array: Sequence[ConfigTree]) -> str:
-    if not array:
-        return "none"
-
-    items = [format_tree(tree) for tree in array]
-    return "\n                                     ".join(items)
+def line_join_list(array: Iterable[object]) -> str:
+    return "\n                                     ".join(str(item) for item in array) if array else "none"
 
 
 def format_bytes(num_bytes: int) -> str:
@@ -3493,7 +3498,7 @@ def bold(s: Any) -> str:
            Repo Signature/Key check: {yes_no(config.repository_key_check)}
                        Repositories: {line_join_list(config.repositories)}
              Use Only Package Cache: {config.cacheonly}
-              Package Manager Trees: {line_join_tree_list(config.package_manager_trees)}
+              Package Manager Trees: {line_join_list(config.package_manager_trees)}
 
     {bold("OUTPUT")}:
                       Output Format: {config.output_format}
@@ -3522,8 +3527,8 @@ def bold(s: Any) -> str:
                  With Documentation: {yes_no(config.with_docs)}
 
                          Base Trees: {line_join_list(config.base_trees)}
-                     Skeleton Trees: {line_join_tree_list(config.skeleton_trees)}
-                        Extra Trees: {line_join_tree_list(config.extra_trees)}
+                     Skeleton Trees: {line_join_list(config.skeleton_trees)}
+                        Extra Trees: {line_join_list(config.extra_trees)}
 
                     Remove Packages: {line_join_list(config.remove_packages)}
                        Remove Files: {line_join_list(config.remove_files)}
@@ -3535,7 +3540,7 @@ def bold(s: Any) -> str:
                       Build Scripts: {line_join_list(config.build_scripts)}
                 Postinstall Scripts: {line_join_list(config.postinst_scripts)}
                    Finalize Scripts: {line_join_list(config.finalize_scripts)}
-                      Build Sources: {line_join_tree_list(config.build_sources)}
+                      Build Sources: {line_join_list(config.build_sources)}
             Build Sources Ephemeral: {yes_no(config.build_sources_ephemeral)}
                  Script Environment: {line_join_list(env)}
                   Environment Files: {line_join_list(config.environment_files)}
@@ -3608,8 +3613,10 @@ def bold(s: Any) -> str:
             Tools Tree Distribution: {none_to_none(config.tools_tree_distribution)}
                  Tools Tree Release: {none_to_none(config.tools_tree_release)}
                   Tools Tree Mirror: {none_to_default(config.tools_tree_mirror)}
+            Tools Tree Repositories: {line_join_list(config.tools_tree_repositories)}
+   Tools Tree Package Manager Trees: {line_join_list(config.tools_tree_package_manager_trees)}
                 Tools Tree Packages: {line_join_list(config.tools_tree_packages)}
-                      Runtime Trees: {line_join_tree_list(config.runtime_trees)}
+                      Runtime Trees: {line_join_list(config.runtime_trees)}
                        Runtime Size: {format_bytes_or_none(config.runtime_size)}
                     Runtime Scratch: {config.runtime_scratch}
                     SSH Signing Key: {none_to_none(config.ssh_key)}


Test patch:
diff --git a/tests/test_json.py b/tests/test_json.py
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -285,8 +285,17 @@ def test_config() -> None:
             "ToolsTree": null,
             "ToolsTreeDistribution": null,
             "ToolsTreeMirror": null,
+            "ToolsTreePackageManagerTrees": [
+                {
+                    "source": "/a/b/c",
+                    "target": "/"
+                }
+            ],
             "ToolsTreePackages": [],
             "ToolsTreeRelease": null,
+            "ToolsTreeRepositories": [
+                "abc"
+            ],
             "UseSubvolumes": "auto",
             "VerityCertificate": "/path/to/cert",
             "VerityKey": null,
@@ -416,8 +425,10 @@ def test_config() -> None:
         tools_tree = None,
         tools_tree_distribution = None,
         tools_tree_mirror = None,
+        tools_tree_package_manager_trees = [ConfigTree(Path("/a/b/c"), Path("/"))],
         tools_tree_packages = [],
         tools_tree_release = None,
+        tools_tree_repositories = ["abc"],
         use_subvolumes = ConfigFeature.auto,
         verity_certificate = Path("/path/to/cert"),
         verity_key = None,