#!/usr/bin/python3 # # makeupdates - Generate an updates.img containing changes since the last # tag, but only changes to the main anaconda runtime. # initrd/stage1 updates have to be created separately. # # Copyright (C) 2019 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . import os import shutil import subprocess import sys import re import glob import argparse import tempfile RPM_FOLDER_NAME = os.path.expanduser("~/.anaconda_updates_rpm_cache") RPM_RELEASE_DIR_TEMPLATE = "for_%s" # The Python site-packages path for pyanaconda. SITE_PACKAGES_PATH = "./usr/lib64/python3.12/site-packages/" # Anaconda scripts that should be installed into the libexec folder LIBEXEC_SCRIPTS = ["log-capture", "start-module", "apply-updates", "anaconda-pre-log-gen", "run-in-new-session"] # Anaconda scripts that should be installed into /usr/bin USR_BIN_SCRIPTS = ["anaconda-disable-nm-ibft-plugin", "anaconda-nm-disable-autocons"] def get_archive_tag(configure, spec): tag = "" with open(configure, "r") as f: for line in f: if line.startswith('AC_INIT('): fields = line.split('[') tag += fields[1].split(']')[0] + '-' + fields[2].split(']')[0] break else: continue with open(spec, "r") as f: for line in f: if line.startswith('Release:'): release = '-' + line.split()[1].split('%')[0] if "@PACKAGE_RELEASE@" not in release: tag += release else: continue return tag def get_archive_tag_offset(configure, spec, offset): tag = get_archive_tag(configure, spec) if not tag.count("-") >= 2: return tag ldash = tag.rfind("-") bldash = tag[:ldash].rfind("-") ver = tag[bldash+1:ldash] if not ver.count(".") >= 1: return tag ver = ver[:ver.rfind(".")] if not len(ver) > 0: return tag globstr = "refs/tags/" + tag[:bldash+1] + ver + ".*" proc = subprocess.Popen(['git', 'for-each-ref', '--sort=taggerdate', '--format=%(tag)', globstr], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() lines = proc[0].strip("\n").split('\n') lines.reverse() try: return lines[offset] except IndexError: return tag def get_anaconda_version(): """Get current anaconda version as string from the configure script""" with open("configure.ac") as f: match = re.search(r"AC_INIT\(\[.*\],\ \[(.*)\],\ \[.*\]\)", f.read()) return match.groups()[0] def get_fedora_version(): """Return integer representing current Fedora number, based on Anaconda version""" anaconda_version = get_anaconda_version() return int(anaconda_version.split(".")[0]) def get_req_tuple(pkg_tuple, version_request): """Return package version requirements tuple :param pkg_tuple: package metadata tuple :type pkg_tuple: tuple :param version_request: version request constant or None :returns: version request tuple :rtype: tuple """ name, _arch, epoch, version, release = pkg_tuple return (name, version_request, (epoch, version, release)) def do_git_diff(tag, args=None): if args is None: args=[] cmd = ['git', 'diff', '--name-status', tag] + args proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, rc = proc.communicate() output = output.decode("utf-8") if proc.returncode: raise RuntimeError("Error running %s: %s" % (" ".join(cmd), rc)) lines = output.split('\n') return lines def do_git_content_diff(tag, args=None): if args is None: args = [] cmd = ['git', 'diff', tag] + args proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, rc = proc.communicate() output = output.decode("utf-8") if rc: raise RuntimeError("Error running %s: %s" % (" ".join(cmd), rc)) lines = output.split('\n') return lines def create_RPM_cache_folder(): """Create RPM package cache folder if it does not already exist""" if not os.path.exists(RPM_FOLDER_NAME): os.makedirs(RPM_FOLDER_NAME) def copy_updated_files(tag, updates, cwd, builddir): def install_to_dir(fname, relpath): sys.stdout.write("Including %s\n" % fname) outdir = os.path.join(updates, relpath) if not os.path.isdir(outdir): os.makedirs(outdir) if os.path.isdir(fname): shutil.copytree(fname, outdir, dirs_exist_ok=True) else: shutil.copy2(fname, outdir) def install_gschema(): # Run make install to a temp directory and pull the compiled file out # of it tmpdir = tempfile.mkdtemp() try: os.system('make -C %s/data/window-manager/config install DESTDIR=%s' % (builddir,tmpdir)) # Find the .compiled file for root, _dirs, files in os.walk(tmpdir): for f in files: if f.endswith('.compiled'): install_to_dir(os.path.join(root, f), 'usr/share/anaconda/window-manager/glib-2.0/schemas') finally: shutil.rmtree(tmpdir) try: lines = do_git_diff(tag) except RuntimeError as e: print("ERROR: %s" % e) return for line in lines: fields = line.split() if len(fields) < 2: continue status = fields[0] gitfile = fields[1] # R is followed by a number that doesn't matter to us. if status == "D" or status[0] == "R": if gitfile.startswith('pyanaconda/') and gitfile.endswith(".py"): sys.stdout.write("Replacing %s\n" % gitfile) file_path = os.path.join(updates, SITE_PACKAGES_PATH, gitfile) if not os.path.exists(os.path.dirname(file_path)): os.makedirs(os.path.dirname(file_path)) with open(file_path, "w") as fobj: fobj.write('from pyanaconda.errors import RemovedModuleError\n') fobj.write('raise RemovedModuleError("This module no longer exists!")\n') if status == "D": continue elif status[0] == "R": gitfile = fields[2] if gitfile.endswith('.spec.in') or (gitfile.find('Makefile') != -1) or \ gitfile.endswith('.c') or gitfile.endswith('.h') or \ gitfile.endswith('.sh'): continue if gitfile.endswith('.glade'): # Some UI files should go under ui/ where dir is the # directory above the file.glade dir_parts = os.path.dirname(gitfile).split(os.path.sep) g_idx = dir_parts.index("gui") uidir = os.path.sep.join(dir_parts[g_idx+1:]) install_to_dir(gitfile, os.path.join("usr/share/anaconda/ui/", uidir)) elif gitfile.startswith('pyanaconda/'): dirname = os.path.join(SITE_PACKAGES_PATH, os.path.dirname(gitfile)) install_to_dir(gitfile, dirname) elif gitfile == 'anaconda.py': # Install it as /usr/sbin/anaconda sys.stdout.write("Including %s\n" % gitfile) if not os.path.isdir(os.path.join(updates, "usr/sbin")): os.makedirs(os.path.join(updates, "usr/sbin")) shutil.copy2(gitfile, os.path.join(updates, "usr/sbin/anaconda")) elif gitfile.startswith("data/systemd"): # include systemd services, but not for example dbus .service files if gitfile.endswith('.service') or gitfile.endswith(".target"): # same for systemd services install_to_dir(gitfile, "usr/lib/systemd/system") elif gitfile.endswith('/anaconda-generator'): # yeah, this should probably be more clever.. install_to_dir(gitfile, "usr/lib/systemd/system-generators") elif gitfile == "data/tmux.conf": install_to_dir(gitfile, "usr/share/anaconda") elif gitfile == "data/anaconda-gtk.css": install_to_dir(gitfile, "usr/share/anaconda") elif gitfile == "data/interactive-defaults.ks": install_to_dir(gitfile, "usr/share/anaconda") elif gitfile == "data/anaconda_options.txt": install_to_dir(gitfile, "usr/share/anaconda") elif gitfile == "data/anaconda.conf": install_to_dir(gitfile, "etc/anaconda") elif gitfile.startswith("data/conf.d"): install_to_dir(gitfile, "etc/anaconda/conf.d") elif gitfile.startswith("data/profile.d"): install_to_dir(gitfile, "etc/anaconda/profile.d") elif gitfile == "data/liveinst/liveinst": install_to_dir(gitfile, "usr/bin") elif gitfile.startswith("data/liveinst/gnome"): install_to_dir(gitfile, "usr/share/anaconda/gnome") elif gitfile.startswith("data/pixmaps"): install_to_dir(gitfile, "usr/share/anaconda/pixmaps") elif gitfile.startswith("widgets/data/pixmaps"): install_to_dir(gitfile, "usr/share/anaconda/pixmaps") elif gitfile.startswith("data/ui/"): install_to_dir(gitfile, "usr/share/anaconda/ui") elif gitfile.startswith("data/window-manager/config"): install_gschema() elif gitfile.startswith("data/post-scripts/"): install_to_dir(gitfile, "usr/share/anaconda/post-scripts") elif gitfile == "utils/handle-sshpw": install_to_dir(gitfile, "usr/sbin") elif any(gitfile.endswith(libexec_script) for libexec_script in LIBEXEC_SCRIPTS): install_to_dir(gitfile, "usr/libexec/anaconda") elif any(gitfile.endswith(usr_bin_script) for usr_bin_script in USR_BIN_SCRIPTS): install_to_dir(gitfile, "usr/bin") elif gitfile.endswith("AnacondaWidgets.py"): import gi install_to_dir(gitfile, gi._overridesdir[1:]) elif gitfile.startswith("data/dbus"): # add DBUS service and config files if gitfile.endswith("anaconda-bus.conf"): install_to_dir(gitfile, "usr/share/anaconda/dbus") elif gitfile.endswith(".service"): install_to_dir(gitfile, "usr/share/anaconda/dbus/services") elif gitfile.endswith(".conf"): install_to_dir(gitfile, "usr/share/anaconda/dbus/confs") def _compilableChanged(tag, compilable): try: lines = do_git_diff(tag, [compilable]) except RuntimeError as e: print("ERROR: %s" % e) return for line in lines: fields = line.split() if len(fields) < 2: continue status = fields[0] gitfile = fields[1] if status == "D": continue if gitfile.startswith('Makefile') or gitfile.endswith('.h') or \ gitfile.endswith('.c') or gitfile.endswith('.py'): return True return False def widgets_changed(tag): return _compilableChanged(tag, 'widgets') def check_autotools(srcdir, builddir): # Assumes that cwd is srcdir if not os.path.isfile(os.path.join(builddir, 'Makefile')): if not os.path.isfile('configure'): os.system('./autogen.sh') os.chdir(builddir) os.system(os.path.join(srcdir, 'configure') + ' --prefix=`rpm --eval %_prefix`') os.chdir(srcdir) def generate_dbus_code(srcdir): os.system('gdbus-codegen ' '--interface-prefix org.fedoraproject.Anaconda.Modules. ' '--c-namespace An ' '--generate-c-code an-localization ' '--output-directory %s/widgets/src ' '%s/widgets/src/dbus/org.fedoraproject.Anaconda.Modules.Localization.xml' % (srcdir, srcdir)) def copy_updated_widgets(updates, srcdir, builddir): os.chdir(srcdir) if os.path.isdir("/usr/lib64"): libdir = "/usr/lib64/" else: libdir = "/usr/lib/" if not os.path.isdir(updates + libdir): os.makedirs(updates + libdir) if not os.path.isdir(updates + libdir + "girepository-1.0"): os.makedirs(updates + libdir + "girepository-1.0") check_autotools(srcdir, builddir) os.system('make -C %s' % builddir) libglob = os.path.normpath(builddir + "/widgets/src/.libs") + "/libAnacondaWidgets.so*" for path in glob.glob(libglob): if os.path.islink(path) and not os.path.exists(updates + libdir + os.path.basename(path)): os.symlink(os.readlink(path), updates + libdir + os.path.basename(path)) elif os.path.isfile(path): shutil.copy2(path, updates + libdir) typeglob = os.path.realpath(builddir + "/widgets/src") + "/AnacondaWidgets-*.typelib" for typelib in glob.glob(typeglob): if os.path.isfile(typelib): shutil.copy2(typelib, updates + libdir + "girepository-1.0") def copy_translations(updates, srcdir, builddir): localedir = "/usr/share/locale/" # Ensure all the message files are up to date if os.system('make -C %s/po' % builddir) != 0: sys.exit(1) # From here gettext puts everything in $srcdir # For each language in LINGUAS, install srcdir/.mo as # /usr/share/locale/$language/LC_MESSAGES/anaconda.mo with open(srcdir + '/po/LINGUAS') as linguas: for line in linguas.readlines(): if line.startswith('#'): continue for lang in line.strip().split(" "): if not os.path.isdir(updates + localedir + lang + "/LC_MESSAGES"): os.makedirs(updates + localedir + lang + "/LC_MESSAGES") shutil.copy2(srcdir + "/po/" + lang + ".mo", updates + localedir + lang + "/LC_MESSAGES/anaconda.mo") def add_rpms(updates_path, rpms): """Add content one or more RPM packages to the updates image :param updates_path: path to the updates image folder :type updates_path: string :param rpms: list of paths to RPM files :type rpms: list of strings """ # convert all the RPM paths to absolute paths, so that # relative paths can be used with -a/--add rpms = map(os.path.abspath, rpms) # resolve wildcards and also eliminate non-existing RPMs resolved_rpms = [] for rpm in rpms: resolved_path = glob.glob(rpm) if not(resolved_path): print("warning: requested rpm %s does not exist and can't be aded" % rpm) elif len(resolved_path) > 1: print("wildcard %s resolved to %d paths" % (rpm, len(resolved_path))) resolved_rpms.extend(resolved_path) for rpm in resolved_rpms: cmd = "cd %s && rpm2cpio %s | cpio -dium" % (updates_path, rpm) sys.stdout.write(cmd+"\n") ret = os.system(cmd) if ret != 0: raise RuntimeError(f"RPM {rpm} can't be added to the updates image!") def create_updates_image(cwd, updates): os.chdir(updates) os.system("find . | cpio -c -o | pigz -9cv > %s/updates.img" % (cwd,)) sys.stdout.write("updates.img ready\n") class ExtendAction(argparse.Action): """ A parsing action that extends a list of items instead of appending to it. Useful where there is an option that can be used multiple times, and each time the values yielded are a list, and a single list is desired. """ def __call__(self, parser, namespace, values, option_string=None): setattr(namespace, self.dest, getattr(namespace, self.dest, []) + values) def main(): cwd = os.getcwd() configure = os.path.realpath(os.path.join(cwd, 'configure.ac')) spec = os.path.realpath(os.path.join(cwd, 'anaconda.spec.in')) updates = os.path.join(cwd, 'updates') parser = argparse.ArgumentParser(description="Make Anaconda updates image") parser.add_argument('-k', '--keep', action='store_true', help='do not delete updates subdirectory') parser.add_argument('-c', '--compile', action='store_true', help='compile code if there are changes in widgets') parser.add_argument('-t', '--tag', action='store', type=str, help='make updates image from TAG to HEAD') parser.add_argument('-o', '--offset', action='store', type=int, default=0, help='make image from (latest_tag - OFFSET) to HEAD') parser.add_argument('-p', '--po', action='store_true', help='update translations') parser.add_argument('-a', '--add', action=ExtendAction, type=str, nargs='+', dest='add_rpms', metavar='PATH_TO_RPM', default=[], help='add contents of RPMs to the updates image (glob supported)') parser.add_argument('-b', '--builddir', action='store', type=str, metavar='BUILDDIR', help='build directory for shared objects') args = parser.parse_args() if not os.path.isfile(configure) and not os.path.isfile(spec): sys.stderr.write("You must be at the top level of the anaconda source tree.\n") sys.exit(1) if not args.tag: # add a fake tag to the arguments to be consistent if args.offset < 1: args.tag = get_archive_tag(configure, spec) else: args.tag = get_archive_tag_offset(configure, spec, args.offset) sys.stdout.write("Using tag: %s\n" % args.tag) if args.builddir: if os.path.isabs(args.builddir): builddir = args.builddir else: builddir = os.path.join(cwd, args.builddir) else: builddir = cwd print("Using site-packages path: %s\n" % SITE_PACKAGES_PATH) if not os.path.isdir(updates): os.makedirs(updates) copy_updated_files(args.tag, updates, cwd, builddir) if args.compile: generate_dbus_code(cwd) if widgets_changed(args.tag): copy_updated_widgets(updates, cwd, builddir) if args.po: copy_translations(updates, cwd, builddir) if args.add_rpms: args.add_rpms = list(set(args.add_rpms)) print('%d RPMs added manually:' % len(args.add_rpms)) for item in args.add_rpms: print(os.path.basename(item)) if args.add_rpms: add_rpms(updates, args.add_rpms) create_updates_image(cwd, updates) if not args.keep: shutil.rmtree(updates) if __name__ == "__main__": main()