Source code for repomatic.release.manpages
# 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.
"""Packing of the man pages a release attaches.
The release workflow renders one roff `.1` file per command of the
`[tool.repomatic] manpages.script` tree, then attaches them to the release as a
single tarball. {func}`pack_manpages` builds that tarball.
The archive is byte-deterministic: members are sorted and carry no timestamp
or owner, so packing an unchanged tree twice gives the same file, and the same
digest for its attestation.
"""
from __future__ import annotations
import gzip
import tarfile
TYPE_CHECKING = False
if TYPE_CHECKING:
from pathlib import Path
from typing import Final
MANPAGES_ARCHIVE_SUFFIX: Final[str] = ".tar.gz"
"""Extension of the man-page tarball, after its asset name."""
_MEMBER_MODE: Final[int] = 0o644
"""Permission bits every archived page carries, whatever the checkout gave it."""
[docs]
def pack_manpages(man_dir: Path, archive: Path) -> list[str]:
"""Bundle the pages of *man_dir* into the gzipped tarball *archive*.
Each member keeps its path below *man_dir*, prefixed with the name of the
directory itself, as `tar --create man/` would store it. A reader unpacking
the archive gets the same `man/` tree the release job rendered.
:param man_dir: Directory holding the rendered pages.
:param archive: The tarball to write. An existing file is replaced.
:return: The member names, in archive order.
:raises ValueError: When *man_dir* holds no file.
"""
pages = sorted(path for path in man_dir.rglob("*") if path.is_file())
if not pages:
msg = f"{man_dir} holds no man page to pack."
raise ValueError(msg)
members = []
with (
archive.open("wb") as raw,
# A fixed modification time and an empty name keep the gzip header
# free of anything that changes from one run to the next.
gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed,
tarfile.open(fileobj=compressed, mode="w") as tar,
):
for page in pages:
name = f"{man_dir.name}/{page.relative_to(man_dir).as_posix()}"
info = tar.gettarinfo(str(page), arcname=name)
info.mtime = 0
info.mode = _MEMBER_MODE
info.uid = info.gid = 0
info.uname = info.gname = ""
with page.open("rb") as content:
tar.addfile(info, content)
members.append(name)
return members