Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge evan-goode/bootc branch to master #2203

Merged
merged 7 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .copr/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright David Cantrell <[email protected]>
# SPDX-License-Identifier: GPL-3.0-or-later

# Set the top level source directory
topdir := $(shell realpath $(dir $(lastword $(MAKEFILE_LIST)))/..)

# Install packages before anything else
_install := $(shell dnf install -y git)
_safedir := $(shell git config --global --add safe.directory $(topdir))

# Pass BUILDTYPE=release to generate a release SRPM
BUILDTYPE ?= copr

# Spec file and template
SPEC_TEMPLATE = $(shell ls -1 $(topdir)/*.spec)
SPEC = $(topdir)/$(shell basename $(SPEC_TEMPLATE))

# Replace placeholders in the spec file template
RPMDATE = $(shell date +'%a %b %d %Y')
#RPMAUTHOR = $(shell git log | grep ^Author: | head -n 1 | cut -d ' ' -f 2,3,4)
RPMAUTHOR = David Cantrell <[email protected]>

# Various things we need to generate a tarball
PKG = $(shell rpmspec -P "$(SPEC_TEMPLATE)" | grep ^Name: | awk '{ print $$2; }')
VER = $(shell rpmspec -P "$(SPEC_TEMPLATE)" | grep ^Version: | awk '{ print $$2; }')

ifeq ($(BUILDTYPE),copr)
GITDATE = $(shell date +'%Y%m%d%H%M')
GITHASH = $(shell git rev-parse --short HEAD)
TARBALL_BASENAME = $(PKG)-$(VER)-$(GITDATE)git$(GITHASH)
TAG = HEAD
else
TAG = $(shell git describe --tags --abbrev=0)
endif

ifeq ($(BUILDTYPE),release)
TARBALL_BASENAME = $(PKG)-$(VER)
endif

# Where to insert the changelog entry
STARTING_POINT = $(shell expr $(shell grep -n ^%changelog "$(SPEC)" | cut -d ':' -f 1) + 1)

srpm:
sed -i -e '1i %global source_date_epoch_from_changelog 0' "$(SPEC)"
sed -e 's|%%VERSION%%|$(VER)|g' < "$(SPEC_TEMPLATE)" > "$(SPEC)".new
mv "$(SPEC)".new "$(SPEC)"
ifeq ($(BUILDTYPE),copr)
sed -i -e '/^Release:/ s/1[^%]*/0.1.$(GITDATE)git$(GITHASH)/' "$(SPEC)"
sed -i -e 's|^Source0:.*$$|Source0: $(TARBALL_BASENAME).tar.gz|g' "$(SPEC)"
sed -i -e 's|^%autosetup.*$$|%autosetup -n $(TARBALL_BASENAME)|g' "$(SPEC)"
sed -i -e '$(STARTING_POINT)a\\' "$(SPEC)"
sed -i -e '$(STARTING_POINT)a - Build $(PKG)-$(VER)-$(GITDATE)git$(GITHASH) snapshot' "$(SPEC)"
sed -i -e '$(STARTING_POINT)a * $(RPMDATE) $(RPMAUTHOR) - $(VER)-$(GITDATE)git$(GITHASH)' "$(SPEC)"
endif
git archive \
--format=tar \
--output='$(topdir)/$(TARBALL_BASENAME).tar' \
--prefix='$(TARBALL_BASENAME)/' $(TAG) $(topdir)
gzip -9f $(topdir)/$(TARBALL_BASENAME).tar
rpmbuild \
-bs --nodeps \
--define "_sourcedir $(topdir)" \
--define "_srcrpmdir $(outdir)" \
--define "_rpmdir $(outdir)" "$(SPEC)"
40 changes: 31 additions & 9 deletions dnf/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,28 +205,50 @@ def do_transaction(self, display=()):
else:
self.output.reportDownloadSize(install_pkgs, install_only)

bootc_unlock_requested = False

if trans or self._moduleContainer.isChanged() or \
(self._history and (self._history.group or self._history.env)):
# confirm with user
if self.conf.downloadonly:
logger.info(_("{prog} will only download packages for the transaction.").format(
prog=dnf.util.MAIN_PROG_UPPER))

elif 'test' in self.conf.tsflags:
logger.info(_("{prog} will only download packages, install gpg keys, and check the "
"transaction.").format(prog=dnf.util.MAIN_PROG_UPPER))
if dnf.util._is_bootc_host() and \
os.path.realpath(self.conf.installroot) == "/" and \
not self.conf.downloadonly:
_bootc_host_msg = _("""
*** Error: system is configured to be read-only; for more
*** information run `bootc --help`.
""")
logger.info(_bootc_host_msg)
raise CliError(_("Operation aborted."))

is_bootc_transaction = dnf.util._is_bootc_host() and \
os.path.realpath(self.conf.installroot) == "/" and \
not self.conf.downloadonly

# Handle bootc transactions. `--transient` must be specified if
# /usr is not already writeable.
if is_bootc_transaction:
if self.conf.persistence == "persist":
logger.info(_("Persistent transactions aren't supported on bootc systems."))
raise CliError(_("Operation aborted."))
assert self.conf.persistence in ("auto", "transient")
if not dnf.util._is_bootc_unlocked():
if self.conf.persistence == "auto":
logger.info(_("This bootc system is configured to be read-only. Pass --transient to "
"perform this and subsequent transactions in a transient overlay which "
"will reset when the system reboots."))
raise CliError(_("Operation aborted."))
assert self.conf.persistence == "transient"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will always be a "transient". A few lines above is assert self.conf.persistence in ("auto", "transient"). And "auto" is handled.

But, OK. The extra assert doesn't break anything.

logger.info(_("A transient overlay will be created on /usr that will be discarded on reboot. "
"Keep in mind that changes to /etc and /var will still persist, and packages "
"commonly modify these directories."))
bootc_unlock_requested = True
elif self.conf.persistence == "transient":
raise CliError(_("Transient transactions are only supported on bootc systems."))

if self._promptWanted():
if self.conf.assumeno or not self.output.userconfirm():
raise CliError(_("Operation aborted."))

if bootc_unlock_requested:
dnf.util._bootc_unlock()
else:
logger.info(_('Nothing to do.'))
return
Expand Down
3 changes: 3 additions & 0 deletions dnf/cli/option_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ def _add_general_options(self):
general_grp.add_argument("--downloadonly", dest="downloadonly",
action="store_true", default=False,
help=_("only download packages"))
general_grp.add_argument("--transient", dest="persistence",
action="store_const", const="transient", default=None,
help=_("Use a transient overlay which will reset on reboot"))
general_grp.add_argument("--comment", dest="comment", default=None,
help=_("add a comment to transaction"))
# Updateinfo options...
Expand Down
2 changes: 1 addition & 1 deletion dnf/conf/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ def _configure_from_options(self, opts):
'best', 'assumeyes', 'assumeno', 'clean_requirements_on_remove', 'gpgcheck',
'showdupesfromrepos', 'plugins', 'ip_resolve',
'rpmverbosity', 'disable_excludes', 'color',
'downloadonly', 'exclude', 'excludepkgs', 'skip_broken',
'downloadonly', 'persistence', 'exclude', 'excludepkgs', 'skip_broken',
'tsflags', 'arch', 'basearch', 'ignorearch', 'cacheonly', 'comment']

for name in config_args:
Expand Down
40 changes: 29 additions & 11 deletions dnf/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import os
import pwd
import shutil
import subprocess
import sys
import tempfile
import time
Expand Down Expand Up @@ -642,15 +643,32 @@ def _is_file_pattern_present(specs):


def _is_bootc_host():
"""Returns true is the system is managed as an immutable container,
false otherwise. If msg is True, a warning message is displayed
for the user.
"""
ostree_booted = '/run/ostree-booted'
usr = '/usr/'
# Check if usr is writtable and we are in a running ostree system.
# We want this code to return true only when the system is in locked state. If someone ran
# bootc overlay or ostree admin unlock we would want normal DNF path to be ran as it will be
# temporary changes (until reboot).
return os.path.isfile(ostree_booted) and not os.access(usr, os.W_OK)
"""Returns true is the system is managed as an immutable container, false
otherwise."""
ostree_booted = "/run/ostree-booted"
return os.path.isfile(ostree_booted)


def _is_bootc_unlocked():
"""Check whether /usr is writeable, e.g. if we are in a normal mutable
system or if we are in a bootc after `bootc usr-overlay` or `ostree admin
unlock` was run."""
usr = "/usr"
return os.access(usr, os.W_OK)


def _bootc_unlock():
"""Set up a writeable overlay on bootc systems."""

if _is_bootc_unlocked():
return

unlock_command = ["bootc", "usr-overlay"]

try:
completed_process = subprocess.run(unlock_command, text=True)
completed_process.check_returncode()
except FileNotFoundError:
raise dnf.exceptions.Error(_("bootc command not found. Is this a bootc system?"))
except subprocess.CalledProcessError:
raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr))