From c7466454bb2bab06625745c9716840b60b18962f Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Tue, 10 Sep 2024 14:26:08 +0200 Subject: [PATCH] Introduce History= setting 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 | 10 ++++++-- mkosi/config.py | 48 ++++++++++++++++++++++++++++++++++-- mkosi/resources/man/mkosi.md | 14 +++++++++++ tests/test_json.py | 2 ++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 962187351..956347530 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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 diff --git a/mkosi/config.py b/mkosi/config.py index def95065e..a8218bacf 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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)} diff --git a/mkosi/resources/man/mkosi.md b/mkosi/resources/man/mkosi.md index f0c7e7c28..3320a8077 100644 --- a/mkosi/resources/man/mkosi.md +++ b/mkosi/resources/man/mkosi.md @@ -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=` diff --git a/tests/test_json.py b/tests/test_json.py index cf065d7d5..c2a044c6a 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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", -- 2.47.2