]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Introduce History= setting 3012/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 10 Sep 2024 12:26:08 +0000 (14:26 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 11 Sep 2024 08:13:52 +0000 (10:13 +0200)
Currently, to boot an image with mkosi qemu after building it with
mkosi build, various settings have to be identical to when mkosi build
was invoked to make sure that mkosi can find the outputs of the previous
build. Because this is rather error prone and annoying, let's introduce
a History= setting to allow mkosi to remember the configuration of the
last build which can then be read again when running a verb that operates
on a built image.

Another case where this is extremely useful is when some part of the
configuration changes every single time, for example if mkosi.version
is an executable script that outputs the current time, which is then
encoded in the output name, we have to remember the previous config,
otherwise mkosi wouldn't be able to find the outputs of the previous
build.

Note that while we load the configuration of the previous build, we
ignore all settings from the [Host] section which we read again from
the configuration files, as the user should be able to change these
without rebuilding the image.

mkosi/__init__.py
mkosi/config.py
mkosi/resources/man/mkosi.md
tests/test_json.py

index 96218735156b2e35dc479d055d5ceae2f2c25aa5..95634753037a56b6d5ca2ba4332ab9b5e27c9308 100644 (file)
@@ -3991,6 +3991,8 @@ def run_verb(args: Args, images: Sequence[Config], *, resources: Path) -> None:
         for config in images:
             run_clean(args, config, resources=resources)
 
+        rmtree(Path(".mkosi-private"))
+
         return
 
     assert args.verb.needs_build()
@@ -4096,8 +4098,12 @@ def run_verb(args: Args, images: Sequence[Config], *, resources: Path) -> None:
                         package_dir=Path(package_dir),
                     )
 
-            if args.auto_bump:
-                bump_image_version()
+        if args.auto_bump:
+            bump_image_version()
+
+        if last.history:
+            Path(".mkosi-private/history").mkdir(parents=True, exist_ok=True)
+            Path(".mkosi-private/history/latest.json").write_text(last.to_json())
 
     if args.verb == Verb.build:
         return
index def95065e5592c7d2d7351e2c175a18d7e3cb459..a8218bacf4d52c7e65aa3a69390ac3586441b129 100644 (file)
@@ -1546,6 +1546,7 @@ class Config:
     build_dir: Optional[Path]
     use_subvolumes: ConfigFeature
     repart_offline: bool
+    history: bool
 
     proxy_url: Optional[str]
     proxy_exclude: list[str]
@@ -2874,6 +2875,13 @@ SETTINGS = (
         default=True,
         scope=SettingScope.universal,
     ),
+    ConfigSetting(
+        dest="history",
+        metavar="BOOL",
+        section="Build",
+        parse=config_parse_boolean,
+        help="Whether mkosi can store information about previous builds",
+    ),
 
     ConfigSetting(
         dest="proxy_url",
@@ -3444,6 +3452,7 @@ class ParseContext:
         # Compare inodes instead of paths so we can't get tricked by bind mounts and such.
         self.includes: set[tuple[int, int]] = set()
         self.immutable: set[str] = set()
+        self.only_sections: tuple[str, ...] = tuple()
 
     def expand_specifiers(self, text: str, path: Path) -> str:
         percent = False
@@ -3683,6 +3692,9 @@ class ParseContext:
                 ):
                     continue
 
+                if self.only_sections and s.section not in self.only_sections:
+                    continue
+
                 for f in s.paths:
                     extra = parse_path(
                         f,
@@ -3708,7 +3720,7 @@ class ParseContext:
             files = getattr(self.config, 'files')
             files += [abs_path]
 
-            for section, k, v in parse_ini(path, only_sections={s.section for s in SETTINGS}):
+            for section, k, v in parse_ini(path, only_sections=self.only_sections or {s.section for s in SETTINGS}):
                 if not k and not v:
                     continue
 
@@ -3795,7 +3807,6 @@ def parse_config(argv: Sequence[str] = (), *, resources: Path = Path("/")) -> tu
     # First, we parse the command line arguments into a separate namespace.
     argparser = create_argument_parser()
     argparser.parse_args(argv, context.cli)
-    context.parse_new_includes()
     args = load_args(context.cli)
 
     # If --debug was passed, apply it as soon as possible.
@@ -3809,6 +3820,35 @@ def parse_config(argv: Sequence[str] = (), *, resources: Path = Path("/")) -> tu
     if not args.verb.needs_config():
         return args, ()
 
+    if (
+        args.verb.needs_build() and
+        args.verb != Verb.build and
+        not args.force and
+        Path(".mkosi-private/history/latest.json").exists()
+    ):
+        prev = Config.from_json(Path(".mkosi-private/history/latest.json").read_text())
+
+        # If we're operating on a previously built image (qemu, boot, shell, ...), we're not rebuilding the
+        # image and the configuration of the latest build is available, we load the config that was used to build the
+        # previous image from there instead of parsing configuration files, except for the Host section settings which
+        # we allow changing without requiring a rebuild of the image.
+        for s in SETTINGS:
+            if s.section in ("Include", "Host"):
+                continue
+
+            if hasattr(context.cli, s.dest) and getattr(context.cli, s.dest) != getattr(prev, s.dest):
+                logging.warning(f"Ignoring {s.long} from the CLI. Run with -f to rebuild the image with this setting")
+
+            setattr(context.cli, s.dest, getattr(prev, s.dest))
+            if hasattr(context.config, s.dest):
+                delattr(context.config, s.dest)
+
+        context.only_sections = ("Include", "Host",)
+    else:
+        prev = None
+
+    context.parse_new_includes()
+
     # One of the specifiers needs access to the directory, so make sure it is available.
     setattr(context.config, "directory", args.directory)
     setattr(context.config, "files", [])
@@ -3825,6 +3865,9 @@ def parse_config(argv: Sequence[str] = (), *, resources: Path = Path("/")) -> tu
     for s in SETTINGS:
         setattr(config, s.dest, context.finalize_value(s))
 
+    if prev:
+        return args, (load_config(config),)
+
     images = []
 
     # If Dependencies= was not explicitly specified on the CLI or in the configuration,
@@ -4290,6 +4333,7 @@ def summary(config: Config) -> str:
                     Build Directory: {none_to_none(config.build_dir)}
                      Use Subvolumes: {config.use_subvolumes}
                      Repart Offline: {yes_no(config.repart_offline)}
+                       Save History: {yes_no(config.history)}
 
     {bold("HOST CONFIGURATION")}:
                           Proxy URL: {none_to_none(config.proxy_url)}
index f0c7e7c28532b9dbe9121410d9976bdce28cf5ce..3320a807782c6f69d73208cffb36a1c3f8ea9244 100644 (file)
@@ -1385,6 +1385,20 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
     devices have to be used to ensure the SELinux extended attributes end
     up in the generated XFS filesystem.
 
+`History=`, `--history=`
+:   Takes a boolean. If enabled, mkosi will write information about the
+    latest build to the `.mkosi-private` subdirectory in the directory
+    from which it was invoked. This information is then used to restore
+    the config of the latest build when running any verb that needs a
+    build without specifying `--force`.
+
+    To give an example of why this is useful, if you run
+    `mkosi -O my-custom-output-dir -f` followed by `mkosi qemu`, `mkosi`
+    will fail saying the image hasn't been built yet. If you run
+    `mkosi -O my-custom-output-dir --history=yes -f` followed by
+    `mkosi qemu`, it will boot the image built in the previous step as
+    expected.
+
 ### [Host] Section
 
 `ProxyUrl=`, `--proxy-url=`
index cf065d7d5518acb20c0b6f45b14b4a3865a60161..c2a044c6a3d900d16bbc2678b83e4b8fcc1ecf8d 100644 (file)
@@ -142,6 +142,7 @@ def test_config() -> None:
             "FinalizeScripts": [],
             "Format": "uki",
             "ForwardJournal": "/mkosi.journal",
+            "History": true,
             "Hostname": null,
             "Image": "default",
             "ImageId": "myimage",
@@ -392,6 +393,7 @@ def test_config() -> None:
         files=[],
         finalize_scripts=[],
         forward_journal=Path("/mkosi.journal"),
+        history=True,
         hostname=None,
         vmm=Vmm.qemu,
         image="default",