diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 668e4201d..69ae8726e 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -27,6 +27,7 @@ from pathlib import Path from typing import Any, Optional, Union, cast +from mkosi._version import __version__ from mkosi.archive import can_extract_tar, extract_tar, make_cpio, make_tar from mkosi.bootloader import ( efi_boot_binary, @@ -128,7 +129,6 @@ MOUNT_ATTR_RDONLY, MS_REC, MS_SLAVE, - __version__, acquire_privileges, have_effective_cap, join_new_session_keyring, @@ -600,6 +600,7 @@ def run_configure_scripts(config: Config) -> Config: MKOSI_UID=str(os.getuid()), MKOSI_GID=str(os.getgid()), MKOSI_DEBUG=one_zero(ARG_DEBUG.get()), + MKOSI_VERSION=__version__, ) if config.profiles: @@ -641,6 +642,7 @@ def run_sync_scripts(config: Config) -> None: MKOSI_UID=str(os.getuid()), MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", + MKOSI_VERSION=__version__, CACHED=one_zero(have_cache(config)), MKOSI_DEBUG=one_zero(ARG_DEBUG.get()), ) @@ -760,6 +762,7 @@ def run_prepare_scripts(context: Context, build: bool) -> None: MKOSI_UID=str(os.getuid()), MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", + MKOSI_VERSION=__version__, WITH_DOCS=one_zero(context.config.with_docs), WITH_NETWORK=one_zero(context.config.with_network), WITH_TESTS=one_zero(context.config.with_tests), @@ -830,6 +833,7 @@ def run_build_scripts(context: Context) -> None: MKOSI_UID=str(os.getuid()), MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", + MKOSI_VERSION=__version__, WITH_DOCS=one_zero(context.config.with_docs), WITH_NETWORK=one_zero(context.config.with_network), WITH_TESTS=one_zero(context.config.with_tests), @@ -905,6 +909,7 @@ def run_postinst_scripts(context: Context) -> None: MKOSI_UID=str(os.getuid()), MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", + MKOSI_VERSION=__version__, WITH_NETWORK=one_zero(context.config.with_network), MKOSI_DEBUG=one_zero(ARG_DEBUG.get()), ) @@ -974,6 +979,7 @@ def run_finalize_scripts(context: Context) -> None: MKOSI_UID=str(os.getuid()), MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", + MKOSI_VERSION=__version__, WITH_NETWORK=one_zero(context.config.with_network), MKOSI_DEBUG=one_zero(ARG_DEBUG.get()), ) @@ -1037,6 +1043,7 @@ def run_postoutput_scripts(context: Context) -> None: MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", MKOSI_DEBUG=one_zero(ARG_DEBUG.get()), + MKOSI_VERSION=__version__, ) if context.config.profiles: @@ -4569,6 +4576,7 @@ def run_clean_scripts(config: Config) -> None: MKOSI_GID=str(os.getgid()), MKOSI_CONFIG="/work/config.json", MKOSI_DEBUG=one_zero(ARG_DEBUG.get()), + MKOSI_VERSION=__version__, ) if config.profiles: diff --git a/mkosi/_staticversion.py b/mkosi/_staticversion.py new file mode 100644 index 000000000..edd965ddb --- /dev/null +++ b/mkosi/_staticversion.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +__version__ = "25.3.post0" diff --git a/mkosi/_version.py b/mkosi/_version.py new file mode 100644 index 000000000..008100977 --- /dev/null +++ b/mkosi/_version.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# The __version__ generation here supports the following modes +# 1. The version is obtained from the environment variable MKOSI_VERSION, to trump all other e.g. for +# debugging purposes +# 2. By default the version is obtained von the Python distribution's metadata on installed packages, unless +# a. the installed version of mkosi lacks this file +# b. the path of that file is not equal to the path of this particular file +# 3. If mkosi has not been installed as a Python package or the metadata pertains to a different mkosi than +# is being called the version is +# b. generated from the output of git describe +# c. looked up in a static version file from resources +# 4. If no version can be found, it is set to "0" + +import datetime +import importlib.metadata +import logging +import os +import subprocess +from importlib.metadata import PackageNotFoundError +from pathlib import Path +from typing import Optional + + +def version_from_metadata() -> Optional[str]: + try: + dist = importlib.metadata.distribution("mkosi") + + this_file = dist.locate_file("mkosi/_version.py") + # If the installed version is too old, it might not have the _version.py file + if not this_file.exists(): + return None + + # If the file importlib.metadata thinks we are talking about is not this one, let's pretend we didn't + # find anything at all and fall back + if this_file != Path(__file__): + return None + + return importlib.metadata.version("mkosi") + except PackageNotFoundError: + return None + + +def version_from_git() -> Optional[str]: + try: + p = subprocess.run( + ["git", "describe"], + cwd=Path(__file__).parent.parent, + check=True, + text=True, + capture_output=True, + ) + # output has form like v25.3-244-g8f491df9 when not on a tag, else just the tag + tag, *rest = p.stdout.strip().split("-") + tag = tag.lstrip("v") + if rest: + numcommits, commit = rest + return f"{tag}.post1.dev{numcommits}+{commit}.d{datetime.datetime.now():%Y%m%d}" + + # we are exactly on a tag + return tag + except (subprocess.CalledProcessError, NotADirectoryError, FileNotFoundError): + return None + + +def version_from_static() -> Optional[str]: + try: + import mkosi._staticversion + + return mkosi._staticversion.__version__ + except ImportError: + return None + + +def version_fallback() -> str: + logging.warning("Unable to determine mkosi version") + return "0" + + +__version__ = ( + os.getenv("MKOSI_VERSION") + or version_from_metadata() + or version_from_git() + or version_from_static() + or version_fallback() +) diff --git a/mkosi/config.py b/mkosi/config.py index 9905f323f..8f6712f53 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -30,11 +30,12 @@ from pathlib import Path from typing import Any, Callable, ClassVar, Generic, Optional, Protocol, TypeVar, Union, cast +from mkosi._version import __version__ from mkosi.distributions import Distribution, detect_distribution from mkosi.log import ARG_DEBUG, ARG_DEBUG_SANDBOX, ARG_DEBUG_SHELL, die from mkosi.pager import page from mkosi.run import SandboxProtocol, find_binary, nosandbox, run, sandbox_cmd, workdir -from mkosi.sandbox import Style, __version__ +from mkosi.sandbox import Style from mkosi.user import INVOKING_USER from mkosi.util import ( PathString, diff --git a/mkosi/initrd.py b/mkosi/initrd.py index 45f3ff521..8eadcfcab 100644 --- a/mkosi/initrd.py +++ b/mkosi/initrd.py @@ -14,11 +14,12 @@ from typing import Optional, cast import mkosi.resources +from mkosi._version import __version__ from mkosi.config import DocFormat, InitrdProfile, OutputFormat from mkosi.documentation import show_docs from mkosi.log import ARG_DEBUG, ARG_DEBUG_SHELL, log_notice, log_setup from mkosi.run import find_binary, run, uncaught_exception_handler -from mkosi.sandbox import __version__, umask +from mkosi.sandbox import umask from mkosi.tree import copy_tree, move_tree, rmtree from mkosi.util import PathString, mandatory_variable, resource_path diff --git a/mkosi/resources/man/mkosi.1.md b/mkosi/resources/man/mkosi.1.md index ad6bfd78b..e7064d8c2 100644 --- a/mkosi/resources/man/mkosi.1.md +++ b/mkosi/resources/man/mkosi.1.md @@ -2578,6 +2578,8 @@ Scripts executed by **mkosi** receive the following environment variables: current image. This file can be parsed inside scripts to gain access to all settings for the current image. +* `$MKOSI_VERSION` is the version string of mkosi. + * `$IMAGE_ID` contains the identifier from the `ImageId=` or `--image-id=` setting. * `$IMAGE_VERSION` contains the version from the `ImageVersion=` or `--image-version=` setting. @@ -2608,6 +2610,7 @@ Consult this table for which script receives which environment variables: | `MKOSI_CONFIG` | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | `MKOSI_GID` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | `MKOSI_UID` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `MKOSI_VERSION` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | `OUTPUTDIR` | | | | | ✓ | ✓ | ✓ | ✓ | | `PACKAGEDIR` | | | ✓ | ✓ | ✓ | ✓ | | | | `PROFILES` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | ✓ | diff --git a/mkosi/sandbox.py b/mkosi/sandbox.py index c79b48739..f76fa3450 100755 --- a/mkosi/sandbox.py +++ b/mkosi/sandbox.py @@ -13,8 +13,6 @@ import sys import warnings # noqa: F401 (loaded lazily by os.execvp() which happens too late) -__version__ = "26~devel" - # The following constants are taken from the Linux kernel headers. AT_EMPTY_PATH = 0x1000 AT_FDCWD = -100 @@ -897,8 +895,19 @@ def main() -> None: print(HELP, file=sys.stderr) sys.exit(0) elif arg == "--version": - print(__version__, file=sys.stderr) - sys.exit(0) + try: + # This is a cyclic import, but this code is not run at import time. + from mkosi._version import __version__ + + print(__version__, file=sys.stderr) + sys.exit(0) + except ImportError: + try: + print(os.environ["MKOSI_VERSION"], file=sys.stderr) + sys.exit(0) + except KeyError: + print("Cannot determine version", file=sys.stderr) + sys.exit(1) if arg == "--tmpfs": fsops.append(TmpfsOperation(argv.pop())) elif arg == "--dev": diff --git a/pyproject.toml b/pyproject.toml index 92a9fc88c..0829f97f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "setuptools-scm"] +requires = ["setuptools>=64", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [project] @@ -7,7 +7,7 @@ name = "mkosi" authors = [ {name = "mkosi contributors", email = "systemd-devel@lists.freedesktop.org"}, ] -version = "25.3" +dynamic = ["version"] description = "Build Bespoke OS Images" readme = "README.md" requires-python = ">=3.9" @@ -15,7 +15,12 @@ license = {file = "LICENSE"} [project.optional-dependencies] bootable = [ - "pefile >= 2021.9.3", + "pefile >= 2021.9.3", +] +dev = [ + "ruff", + "mypy", + "pyright", ] [project.scripts] @@ -45,6 +50,9 @@ packages = [ "tmpfiles.d/*", ] +[tool.setuptools_scm] +version_scheme = "no-guess-dev" + [tool.isort] profile = "black" include_trailing_comma = true diff --git a/tools/do-a-release.sh b/tools/do-a-release.sh index d1f52ec83..bbd3c88d9 100755 --- a/tools/do-a-release.sh +++ b/tools/do-a-release.sh @@ -13,8 +13,9 @@ if ! git diff-index --quiet HEAD; then exit 1 fi -sed -r -i "s/^version = \".*\"$/version = \"$VERSION\"/" pyproject.toml -sed -r -i "s/^__version__ = \".*\"$/__version__ = \"$VERSION\"/" mkosi/sandbox.py +printf '# SPDX-License-Identifier: LGPL-2.1-or-later\n__version__ = "%s"\n' \ + "${VERSION}.post0" \ + >mkosi/_staticversion.py git add -p pyproject.toml mkosi @@ -25,8 +26,6 @@ git tag -s "v$VERSION" -m "mkosi $VERSION" VERSION_MAJOR=${VERSION%%.*} VERSION="$((VERSION_MAJOR + 1))~devel" -sed -r -i "s/^__version__ = \".*\"$/__version__ = \"$VERSION\"/" mkosi/sandbox.py - git add -p mkosi git commit -m "Bump version to $VERSION" diff --git a/tools/generate-zipapp.sh b/tools/generate-zipapp.sh index e54b0fc25..bbf762ce5 100755 --- a/tools/generate-zipapp.sh +++ b/tools/generate-zipapp.sh @@ -10,6 +10,13 @@ mkdir -p builddir cp -r mkosi "${BUILDDIR}/" +# HACK: importlib metadata doesn't seem to be there in a zipapp even if +# properly installed via pip, so let's patch it in there manually. +mkosiversion="$(python3 -m mkosi --version)" +printf '# SPDX-License-Identifier: LGPL-2.1-or-later\n__version__ = "%s"\n' \ + "${mkosiversion#mkosi }" \ + >"${BUILDDIR}/mkosi/_staticversion.py" + python3 -m zipapp \ -p "/usr/bin/env python3" \ -o builddir/mkosi \