Source code for repomatic.tooling.nuitka
# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""What `repomatic run nuitka` adds to a Nuitka build.
The `nuitka` entry of {data}`~repomatic.tooling.registry.TOOL_REGISTRY` reads
two hooks from here:
- {func}`nuitka_params`, its computed parameters: the modules the binary
leaves out, and the options of the build target this machine compiles for.
- {func}`prepare_nuitka_build`, its preparation: the extras to install, the
compile cache to trim and configure, and the environment of the target.
Each of them reads the machine it runs on, so the release workflow hands a
compile job nothing but the entry point to build.
"""
from __future__ import annotations
import logging
import os
import shutil
from pathlib import Path
from ..deps.uv import run_uv, uv_cmd
from ..release.binary import current_build_target
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Final
from ..metadata.core import Metadata
MEBIBYTE: Final[int] = 1024 * 1024
"""Bytes in a mebibyte, the unit the cache size is logged in."""
COMPILE_CACHE_CAP: Final[int] = 2048 * MEBIBYTE
"""Size past which {func}`prepare_compile_cache` resets the compile cache.
Nuitka's internal clcache prunes by age alone and has no size cap (ccache
prunes itself at 5 GiB), so a restore-then-save cycle can still grow the
cache. Past this size, the cache starts a fresh lineage.
"""
CCACHE_CONFIG: Final[str] = "ccache.conf"
"""The file ccache reads its settings from, inside its cache directory."""
[docs]
def nuitka_params(metadata: Metadata) -> list[str]:
"""Build the options `repomatic run nuitka` passes after the shared ones.
:param metadata: The project the binary is built from.
:return: A `--nofollow-import-to` option per module of `[tool.repomatic]
nuitka.nofollow-imports`, then the options of the build target this
machine compiles for (see
{attr}`~repomatic.release.binary.BuildTarget.nuitka_flags`).
"""
params = [
f"--nofollow-import-to={module}"
for module in metadata.config.nuitka_nofollow_imports
]
target = current_build_target()
if target is not None:
params.extend(target.nuitka_flags)
return params
def _directory_size(directory: Path) -> int:
"""Total size in bytes of the files below *directory*."""
return sum(path.stat().st_size for path in directory.rglob("*") if path.is_file())
[docs]
def prepare_compile_cache(
cache_dir: Path, ccache_dir: Path, work_root: Path | None
) -> None:
"""Trim the Nuitka compile cache, then write the settings of ccache.
Two duties. The trim resets a cache past {data}`COMPILE_CACHE_CAP`. The
settings land in the {data}`CCACHE_CONFIG` file of *ccache_dir*, which
ccache always reads, and which the cache step of the workflow saves with
the objects:
- `compiler_check = content` hashes the compiler binary's content. The
default check reads its modification time and size, which runner VMs do
not keep stable.
- `base_dir` makes ccache hash every path below *work_root* as a relative
path, so a work root that moves between machines keeps its keys. Nuitka
sets no `base_dir` of its own.
:param cache_dir: The directory `NUITKA_CACHE_DIR` names.
:param ccache_dir: The directory `CCACHE_DIR` names.
:param work_root: The directory holding every checkout of the runner, or
`None` to leave `base_dir` unset.
"""
if cache_dir.is_dir():
size = _directory_size(cache_dir)
logging.info(f"Nuitka cache restored at {size // MEBIBYTE} MiB.")
if size > COMPILE_CACHE_CAP:
logging.info(
f"Cache exceeds {COMPILE_CACHE_CAP // MEBIBYTE} MiB, resetting."
)
shutil.rmtree(cache_dir)
ccache_dir.mkdir(parents=True, exist_ok=True)
settings = ["compiler_check = content"]
if work_root is not None:
settings.append(f"base_dir = {work_root}")
(ccache_dir / CCACHE_CONFIG).write_text(
"".join(f"{line}\n" for line in settings), encoding="UTF-8"
)
[docs]
def prepare_nuitka_build(metadata: Metadata) -> dict[str, str]:
"""Ready this machine for the Nuitka build `repomatic run nuitka` starts.
Installs the `[tool.repomatic] nuitka.extras`, then prepares the compile
cache when the job relocated it under `NUITKA_CACHE_DIR` and the target
keeps one (see
{attr}`~repomatic.release.binary.BuildTarget.compile_cache`). Outside CI
the variable is unset, and the cache is left alone.
:param metadata: The project the binary is built from.
:return: The environment variables the build of this machine's target
needs, from {attr}`~repomatic.release.binary.BuildTarget.build_env`.
"""
install_extras(metadata.config.nuitka_extras)
target = current_build_target()
cache_dir = os.environ.get("NUITKA_CACHE_DIR")
if cache_dir and (target is None or target.compile_cache):
workspace = os.environ.get("RUNNER_WORKSPACE")
prepare_compile_cache(
Path(cache_dir),
Path(os.environ.get("CCACHE_DIR") or Path(cache_dir) / "ccache"),
Path(workspace).parent if workspace else None,
)
return dict(target.build_env) if target is not None else {}