/*
 * Copyright (c) 2015, 2016, 2020, 2021 Jonas 'Sortie' Termansen.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * sysupgrade.c
 * Operating system upgrader.
 */

#include <sys/display.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/wait.h>

#include <brand.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fstab.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>

#include <mount/blockdevice.h>
#include <mount/filesystem.h>
#include <mount/harddisk.h>
#include <mount/partition.h>

#include "autoconf.h"
#include "conf.h"
#include "devices.h"
#include "execute.h"
#include "fileops.h"
#include "hooks.h"
#include "interactive.h"
#include "manifest.h"
#include "release.h"

const char* prompt_man_section = "7";
const char* prompt_man_page = "upgrade";

struct installation
{
	struct blockdevice* bdev;
	struct release release;
	struct mountpoint* mountpoints;
	size_t mountpoints_used;
	char* machine;
};

static struct installation* installations;
static size_t installations_count;
static size_t installations_length;
static pid_t main_pid;
static struct mountpoint* mountpoints;
static size_t mountpoints_used;
static bool fs_made = false;
static char fs[] = "/tmp/fs.XXXXXX";
static int exit_gui_code = -1;

static bool add_installation(struct blockdevice* bdev,
                             struct release* release,
                             struct mountpoint* mountpoints,
                             size_t mountpoints_used,
                             char* machine)
{
	if ( installations_count == installations_length )
	{
			size_t length = installations_length;
			if ( !length )
				length = 8;
			struct installation* new_installations = (struct installation*)
				reallocarray(NULL, length, 2 * sizeof(struct installation));
			if ( !new_installations )
				return false;
			installations = new_installations;
			installations_length = 2 * length;
	}
	struct installation* installation = &installations[installations_count++];
	installation->bdev = bdev;
	installation->release = *release;
	installation->mountpoints = mountpoints;
	installation->mountpoints_used = mountpoints_used;
	installation->machine = machine;
	return true;
}

static void search_installation_path(const char* mnt, struct blockdevice* bdev)
{
	char* release_errpath;
	if ( asprintf(&release_errpath, "%s: /etc/sortix-release",
	              path_of_blockdevice(bdev)) < 0 )
	{
		warn("malloc");
		return;
	}
	char* release_path;
	if ( asprintf(&release_path, "%s/etc/sortix-release", mnt) < 0 )
	{
		warn("malloc");
		free(release_errpath);
		return;
	}
	struct release release;
	bool status = os_release_load(&release, release_path, release_errpath);
	free(release_path);
	free(release_errpath);
	if ( !status )
		return;
	char* fstab_path;
	if ( asprintf(&fstab_path, "%s/etc/fstab", mnt) < 0 )
	{
		warn("malloc");
		release_free(&release);
		return;
	}
	struct mountpoint* mountpoints;
	size_t mountpoints_used;
	status = load_mountpoints(fstab_path, &mountpoints, &mountpoints_used);
	free(fstab_path);
	if ( !status )
	{
		warn("%s: %s", path_of_blockdevice(bdev), "/etc/fstab");
		release_free(&release);
		return;
	}
	char* machine_path;
	if ( asprintf(&machine_path, "%s/etc/machine", mnt) < 0 )
	{
		warn("%s: malloc", path_of_blockdevice(bdev));
		free_mountpoints(mountpoints, mountpoints_used);
		release_free(&release);
		return;
	}
	char* machine = read_string_file(machine_path);
	free(machine_path);
	if ( !machine )
	{
		warn("%s/etc/machine", path_of_blockdevice(bdev));
		free_mountpoints(mountpoints, mountpoints_used);
		release_free(&release);
		return;
	}
	if ( !add_installation(bdev, &release, mountpoints, mountpoints_used,
	                       machine) )
	{
		free(machine);
		free_mountpoints(mountpoints, mountpoints_used);
		release_free(&release);
		return;
	}
}

static void search_installation_bdev(const char* mnt, struct blockdevice* bdev)
{
	if ( !bdev->fs )
		return;
	if ( !bdev->fs->driver )
		return;
	struct mountpoint mp = { 0 };
	mp.absolute = (char*) mnt;
	mp.fs = bdev->fs;
	mp.entry.fs_file = (char*) mnt;
	if ( !mountpoint_mount(&mp) )
		return;
	search_installation_path(mnt, bdev);
	mountpoint_unmount(&mp);
}

static void search_installations(const char* mnt)
{
	for ( size_t i = 0; i < installations_count; i++ )
	{
		struct installation* inst = &installations[i];
		free_mountpoints(inst->mountpoints, inst->mountpoints_used);
		release_free(&inst->release);
	}
	free(installations);
	installations_count = 0;
	installations_length = 0;

	for ( size_t i = 0; i < hds_count; i++ )
	{
		struct harddisk* hd = hds[i];
		if ( hd->bdev.pt )
		{
			for ( size_t n = 0; n < hd->bdev.pt->partitions_count; n++ )
			{
				struct partition* p = hd->bdev.pt->partitions[n];
				search_installation_bdev(mnt, &p->bdev);
			}
		}
		else
			search_installation_bdev(mnt, &hd->bdev);
	}
}

static void next_version(const struct release* current, struct release* new)
{
	// Next release of a development snapshot is the final release.
	if ( current->version_dev )
	{
		new->version_major = current->version_major;
		new->version_minor = current->version_minor;
		new->version_dev = false;
		return;
	}

	// Releases increment by 0.1.
	new->version_major = current->version_major;
	new->version_minor = current->version_minor + 1;
	new->version_dev = false;

	// Major increments instead of minor release 10.
	if ( new->version_minor == 10 )
	{
		new->version_major++;
		new->version_minor = 0;
	}
}

static bool downgrading_version(const struct release* old,
                                const struct release* new)
{
	if ( new->version_major < old->version_major )
		return true;
	if ( new->version_major > old->version_major )
		return false;
	if ( new->version_minor < old->version_minor )
		return true;
	if ( new->version_minor > old->version_minor )
		return false;
	if ( new->version_dev && !old->version_dev )
		return true;
	return false;
}

static bool skipping_version(const struct release* old,
                             const struct release* new)
{
	// Not skipping a release if upgrading to older release.
	if ( downgrading_version(old, new) )
		return false;

	// Not skipping a release if upgrading to same release.
	if ( new->version_major == old->version_major &&
	     new->version_minor == old->version_minor &&
	     new->version_dev == old->version_dev )
		return false;

	// Not skipping a release if upgrading to the next release.
	struct release next;
	next_version(old, &next);
	if ( new->version_major == next.version_major &&
	     new->version_minor == next.version_minor )
		return false;

	return true;
}

static void preserve_src(const char* where)
{
	if ( access_or_die(where, F_OK) < 0 )
		return;
	if ( access_or_die("oldsrc", F_OK) < 0 )
	{
		if ( mkdir("oldsrc", 0755) < 0 )
		{
			warn("oldsrc");
			_exit(2);
		}
	}
	time_t now = time(NULL);
	struct tm tm;
	localtime_r(&now, &tm);
	char buf[64];
	snprintf(buf, sizeof(buf), "oldsrc/%s-%i-%02i-%02i",
	         where, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday);
	if ( access_or_die(buf, F_OK) == 0 )
	{
		snprintf(buf, sizeof(buf), "oldsrc/%s-%i-%02i-%02i-%02i-%02i-%02i",
		         where, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
		         tm.tm_hour, tm.tm_min, tm.tm_sec);
		if ( access_or_die(buf, F_OK) == 0 )
		{
			snprintf(buf, sizeof(buf), "oldsrc/%s.XXXXXX", where);
			if ( !mkdtemp(buf) )
			{
				warnx("failed to find location to store old /%s", where);
				_exit(2);
			}
			rmdir(buf);
		}
	}
	printf(" - Moving /%s to /%s\n", where, buf);
	if ( rename(where, buf) < 0 )
	{
		warn("rename: %s -> %s", where, buf);
		_exit(2);
	}
}

void exit_handler(void)
{
	if ( getpid() != main_pid )
		 return;
	chdir("/");
	for ( size_t n = mountpoints_used; n != 0; n-- )
	{
		size_t i = n - 1;
		struct mountpoint* mountpoint = &mountpoints[i];
		mountpoint_unmount(mountpoint);
	}
	if ( fs_made )
		rmdir(fs);
	if ( 0 <= exit_gui_code )
		gui_shutdown(exit_gui_code);
}

void exit_gui(int code)
{
	exit_gui_code = code;
	exit(code);
}

static void cancel_on_sigint(int signum)
{
	(void) signum;
	errx(2, "fatal: Upgrade canceled");
}

int main(void)
{
	shlvl();

	if ( !isatty(0) )
		errx(2, "fatal: stdin is not a terminal");
	if ( !isatty(1) )
		errx(2, "fatal: stdout is not a terminal");
	if ( !isatty(2) )
		errx(2, "fatal: stderr is not a terminal");

	if ( getuid() != 0 )
		errx(2, "You need to be root to install %s", BRAND_DISTRIBUTION_NAME);
	if ( getgid() != 0 )
		errx(2, "You need to be group root to install %s", BRAND_DISTRIBUTION_NAME);

	main_pid = getpid();
	if ( atexit(exit_handler) != 0 )
		err(2, "atexit");

	autoconf_load("/etc/autoupgrade.conf");

	char* accepts_defaults = autoconf_eval("accept_defaults");
	bool non_interactive = accepts_defaults &&
	                       !strcasecmp(accepts_defaults, "yes");
	free(accepts_defaults);

	struct utsname uts;
	uname(&uts);

	static char input[256];

	textf("Hello and welcome to the " BRAND_DISTRIBUTION_NAME " " VERSIONSTR ""
	      " upgrader for %s.\n\n", uts.machine);

	if ( autoconf_has("ready") ||
	     autoconf_has("confirm_upgrade") )
	{
		int countdown = 10;
		if ( autoconf_has("countdown") )
		{
			char* string = autoconf_eval("countdown");
			countdown = atoi(string);
			free(string);
		}
		sigset_t old_set;
		sigset_t new_set;
		sigemptyset(&new_set);
		sigaddset(&new_set, SIGINT);
		sigprocmask(SIG_BLOCK, &new_set, &old_set);
		struct sigaction old_sa;
		struct sigaction new_sa = { 0 };
		new_sa.sa_handler = cancel_on_sigint;
		sigaction(SIGINT, &new_sa, &old_sa);
		for ( ; 0 < countdown; countdown-- )
		{
			textf("Automatically upgrading to " BRAND_DISTRIBUTION_NAME " "
			      VERSIONSTR " in %i %s... (Control-C to cancel)\n", countdown,
			      countdown != 1 ? "seconds" : "second");
			sigprocmask(SIG_SETMASK, &old_set, NULL);
			sleep(1);
			sigprocmask(SIG_BLOCK, &new_set, &old_set);
		}
		textf("Automatically upgrading " BRAND_DISTRIBUTION_NAME " "
			      VERSIONSTR "...\n");
		text("\n");
		sigaction(SIGINT, &old_sa, NULL);
		sigprocmask(SIG_SETMASK, &old_set, NULL);
	}

	// '|' rather than '||' is to ensure side effects.
	if ( missing_program("cut") |
	     missing_program("dash") |
	     missing_program("fsck.ext2") |
	     missing_program("grub-install") |
	     missing_program("man") |
	     missing_program("sed") |
	     missing_program("xargs") )
	{
		text("Warning: This system does not have the necessary third party "
		     "software installed to properly upgrade installations.\n");
		while ( true )
		{
			prompt(input, sizeof(input), "ignore_missing_programs",
			       "Sure you want to proceed?", "no");
			if ( strcasecmp(input, "no") == 0 )
				return 0;
			if ( strcasecmp(input, "yes") == 0 )
				break;
		}
		text("\n");
	}

	text("This program will upgrade an existing installation to this "
	     "version. You can always escape to a shell by answering '!' to any "
	     "regular prompt. You can view the upgrade(7) manual page by answering "
	     "'!man'. Default answers are in []'s and can be selected by pressing "
	     "enter.\n\n");

	const char* readies[] =
	{
		"Ready",
		"Yes",
		"Yeah",
		"Yep",
		"Let's go",
		"Let's do this",
		"Betcha",
		"Sure am",
		"You bet",
		"This time it will listen to my music",
	};
	size_t num_readies = sizeof(readies) / sizeof(readies[0]);
	const char* ready = readies[arc4random_uniform(num_readies)];
	if ( autoconf_has("confirm_upgrade") )
		text("Warning: This upgrader will automatically upgrade an operating "
		     "system!\n");
	prompt(input, sizeof(input), "ready", "Ready?", ready);
	text("\n");

	bool kblayout_setable = 0 <= tcgetblob(0, "kblayout", NULL, 0) ||
	                        getenv("DISPLAY_SOCKET");
	while ( kblayout_setable )
	{
		// TODO: Detect the name of the current keyboard layout.
		prompt(input, sizeof(input), "kblayout",
		       "Choose your keyboard layout ('?' or 'L' for list)", "default");
		if ( !strcmp(input, "?") ||
		     !strcmp(input, "l") ||
		     !strcmp(input, "L") )
		{
			DIR* dir = opendir("/share/kblayout");
			if ( !dir )
			{
				warn("%s", "/share/kblayout");
				continue;
			}
			bool space = false;
			struct dirent* entry;
			while ( (entry = readdir(dir)) )
			{
				if ( entry->d_name[0] == '.' )
					continue;
				if ( space )
					putchar(' ');
				fputs(entry->d_name, stdout);
				space = true;
			}
			closedir(dir);
			if ( !space )
				fputs("(No keyboard layouts available)", stdout);
			putchar('\n');
			continue;
		}
		if ( !strcmp(input, "default") )
			break;
		const char* argv[] = { "chkblayout", "--", input, NULL };
		if ( execute(argv, "f") == 0 )
			break;
	}
	if ( kblayout_setable )
		text("\n");

	struct dispmsg_crtc_mode mode;
	if ( get_video_mode(&mode) )
	{
		bool good = (mode.control & DISPMSG_CONTROL_VALID) &&
		            (mode.control & DISPMSG_CONTROL_GOOD_DEFAULT);
		if ( mode.control & DISPMSG_CONTROL_VM_AUTO_SCALE )
		{
			text("The display resolution will automatically change to "
			     "match the size of the virtual machine window.\n\n");
			good = true;
		}
		const char* def = non_interactive || good ? "no" : "yes";
		while ( true )
		{
			prompt(input, sizeof(input), "videomode",
			       "Select display resolution? "
			       "(yes/no/WIDTHxHEIGHTxBPP)", def);
			unsigned int xres, yres, bpp;
			bool set = sscanf(input, "%ux%ux%u", &xres, &yres, &bpp) == 3;
			if ( !set && strcasecmp(input, "no") && strcasecmp(input, "yes") )
				continue;
			if ( strcasecmp(input, "no") == 0 )
				break;
			const char* r = set ? input : NULL;
			if ( execute((const char*[]) { "chvideomode", r, NULL }, "f") != 0 )
				continue;
			break;
		}
		text("\n");
	}

	struct release new_release;
	if ( !os_release_load(&new_release, "/etc/sortix-release",
	                                    "/etc/sortix-release") )
	{
		if ( errno == ENOENT )
			warn("/etc/sortix-release");
		exit(2);
	}

	if ( !mkdtemp(fs) )
		err(2, "mkdtemp: %s", "/tmp/fs.XXXXXX");
	fs_made = true;
	// Export for the convenience of users escaping to a shell.
	setenv("SYSINSTALL_TARGET", fs, 1);

	struct installation* target = NULL;
	while ( true )
	{
		text("Searching for existing installations...\n");
		scan_devices();
		search_installations(fs);
		text("\n");

		if ( installations_count == 0 )
		{
			while ( true )
			{
				prompt(input, sizeof(input), "run_installer_instead",
					   "No existing installations found, "
					   "run installer instead? (yes/no)", "yes");
				if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") )
					break;
			}
			if ( !strcasecmp(input, "yes") )
			{
				text("\n");
				rmdir(fs);
				execlp("sysinstall", "sysinstall", (const char*) NULL);
				err(2, "sysinstall");
			}
			errx(2, "Upgrade aborted since no installations were found");
		}

		while ( true )
		{
			for ( size_t i = 0; i < installations_count; i++ )
			{
				struct installation* installation = &installations[i];
				printf("  %-16s  %s (%s)\n",
				       path_of_blockdevice(installation->bdev),
				       installation->release.pretty_name,
				       installation->machine);
			}
			text("\n");

			const char* def = NULL;
			if ( installations_count == 1 )
				def = path_of_blockdevice(installations[0].bdev);
			prompt(input, sizeof(input), "which_installaton",
			       "Which installation to upgrade?", def);
			target = NULL;
			for ( size_t i = 0; i < installations_count; i++ )
			{
				struct installation* installation = &installations[i];
				const char* path = path_of_blockdevice(installation->bdev);
				if ( strcmp(input, path) != 0 )
					continue;
				target = installation;
			}
			if ( !target )
			{
				text("Answer was not one of the found devices.\n\n");
				continue;
			}
			break;
		}

		break;
	}
	text("\n");

	struct release* target_release = &target->release;

	char* source_machine = read_string_file("/etc/machine");
	if ( !source_machine )
		err(2, "/etc/machine");
	if ( strcmp(target->machine, source_machine) != 0 )
	{
		textf("Warning: You are changing an existing installation to another "
		      "architecture! (%s -> %s) This is not supported and there is no "
		      "promise this will work!\n", target->machine, source_machine);
		while ( true )
		{
			prompt(input, sizeof(input), "switch_architecture",
			       "Change the existing installation to another architecture?",
			       "no");
			if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") )
				break;
		}
		if ( !strcasecmp(input, "no") )
			errx(2, "upgrade aborted because of architecture mismatch");
		text("\n");
	}
	free(source_machine);

	if ( downgrading_version(target_release, &new_release) )
	{
		text("Warning: You are downgrading an existing installation to an "
		     "earlier release. This is not supported and there is no promise "
		     "this will work!\n\n");

		while ( true )
		{
			prompt(input, sizeof(input), "downgrade_release",
			       "Downgrade to an earlier release?", "no");
			if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") )
				break;
		}
		if ( !strcasecmp(input, "no") )
			errx(2, "Upgrade aborted due to version downgrade");
		text("\n");
	}
	else if ( skipping_version(target_release, &new_release) )
	{
		text("Warning: You are not upgrading this installation to its next "
		     "release. You cannot skip releases. This is not supported and "
		     "there is no promise this will will work!\n\n");

		while ( true )
		{
			prompt(input, sizeof(input), "skip_release",
			       "Skip across releases?", "no");
			if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") )
				break;
		}
		if ( !strcasecmp(input, "no") )
			errx(2, "Upgrade aborted due to skipping releases");
		text("\n");
	}

	if ( abi_compare(new_release.abi_major, new_release.abi_minor,
	                 target_release->abi_major, target_release->abi_minor) < 0 )
	{
		text("Warning: You are downgrading an existing installation to an "
		     "release with an earlier ABI. This is not supported and there is "
		     "no promise this will work!\n\n");

		while ( true )
		{
			prompt(input, sizeof(input), "downgrade_abi",
			       "Downgrade to an earlier ABI?", "no");
			if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") )
				break;
		}
		if ( !strcasecmp(input, "no") )
			errx(2, "Upgrade aborted due to ABI downgrade");
		text("\n");
	}

	bool can_run_old_abi =
		abi_compatible(target_release->abi_major, target_release->abi_minor,
		               new_release.abi_major, new_release.abi_minor);

	mountpoints = target->mountpoints;
	mountpoints_used = target->mountpoints_used;

	struct blockdevice* bdev = target->bdev;
	struct blockdevice* bootloader_bdev = target->bdev;

	for ( size_t i = 0; i < mountpoints_used; i++ )
	{
		struct mountpoint* mnt = &mountpoints[i];
		const char* spec = mnt->entry.fs_spec;
		if ( !(mnt->fs = search_for_filesystem_by_spec(spec)) )
			errx(2, "fstab: %s: Found no mountable filesystem matching `%s'",
			     mnt->entry.fs_file, spec);
		if ( !mnt->fs->driver )
			errx(2, "fstab: %s: %s: Don't know how to mount this %s filesystem",
			     mnt->entry.fs_file,
			     path_of_blockdevice(mnt->fs->bdev),
			     mnt->fs->fstype_name);
	}

	for ( size_t i = 0; i < mountpoints_used; i++ )
	{
		struct mountpoint* mnt = &mountpoints[i];
		if ( !strcmp(mnt->entry.fs_file, "/boot") )
			bootloader_bdev = mnt->fs->bdev;
		char* absolute;
		if ( asprintf(&absolute, "%s%s", fs, mnt->absolute) < 0 )
			err(2, "asprintf");
		free(mnt->absolute);
		mnt->absolute = absolute;
		if ( !mountpoint_mount(mnt) )
			exit(2);
	}

	const char* bdev_path = path_of_blockdevice(bdev);
	const char* bootloader_dev_path = device_path_of_blockdevice(bootloader_bdev);

	if ( chdir(fs) < 0 )
		err(2, "chdir: %s", fs);

	if ( access_or_die("sysmerge", F_OK) == 0 )
	{
		text("Warning: A sysmerge(8) upgrade is scheduled for the next boot. "
		     "You must cancel this to proceed.\n\n");
		if ( !can_run_old_abi )
		{
			text("Error: Can't cancel pending upgrade due to ABI change.\n");
			errx(2, "Upgrade aborted due to pending sysmerge(8) upgrade");
		}

		while ( true )
		{
			prompt(input, sizeof(input), "cancel_pending_sysmerge",
			       "Cancel pending sysmerge upgrade?", "yes");
			if ( !strcasecmp(input, "no") || !strcasecmp(input, "yes") )
				break;
		}
		if ( !strcasecmp(input, "no") )
			errx(2, "Upgrade aborted due to pending sysmerge(8) upgrade");
		text("\n");
		execute((const char*[]) { "chroot", "-d", ".", "sysmerge", "--cancel",
		                          NULL }, "e");
	}

	bool do_upgrade_bootloader;
	struct conf conf;
	conf_init(&conf);
	while ( true )
	{
		conf_free(&conf);
		if ( !conf_load(&conf, "etc/upgrade.conf") && errno != ENOENT )
			err(2, "etc/upgrade.conf");

		do_upgrade_bootloader =
			conf.grub && (conf.ports || (conf.system && can_run_old_abi));

		textf("We are now ready to upgrade to %s %s. Take a moment to verify "
			  "everything is in order.\n", BRAND_DISTRIBUTION_NAME, VERSIONSTR);
		text("\n");
		char abibuf[16];
		printf("  %-16s  system architecture\n", uts.machine);
		printf("  %-16s  root filesystem\n", bdev_path);
		if ( do_upgrade_bootloader )
			printf("  %-16s  bootloader installation target\n", bootloader_dev_path);
		printf("  %-16s  old version\n", target_release->pretty_name);
		printf("  %-16s  new version\n", new_release.pretty_name);
		snprintf(abibuf, sizeof(abibuf), "%lu.%lu",
			     target_release->abi_major, target_release->abi_minor);
		printf("  %-16s  old ABI\n", abibuf);
		snprintf(abibuf, sizeof(abibuf), "%lu.%lu",
			     new_release.abi_major, new_release.abi_minor);
		printf("  %-16s  new ABI\n", abibuf);
		if ( conf.system )
			printf("  %-16s  will be updated\n", "system");
		else
			printf("  %-16s  will not be updated\n", "system");
		if ( conf.ports )
			printf("  %-16s  will be updated\n", "ports");
		else
			printf("  %-16s  will not be updated\n", "ports");
		if ( has_manifest("src") )
		{
			if ( conf.newsrc )
				printf("  %-16s  new source code\n", "/newsrc");
			else if ( conf.src )
				printf("  %-16s  will be updated\n", "/src");
			else
				printf("  %-16s  will not be updated\n", "/src");
		}
		else
			printf("  %-16s  will not be updated\n", "/src");
		if ( do_upgrade_bootloader )
			printf("  %-16s  will be updated\n", "bootloader");
		else
			printf("  %-16s  will not be updated\n", "bootloader");
		text("\n");

		while ( true )
		{
			promptx(input, sizeof(input), "confirm_upgrade",
				   "Upgrade? (yes/no/exit/poweroff/reboot/halt)", "yes", true);
			if ( !strcasecmp(input, "yes") )
				break;
			else if ( !strcasecmp(input, "no") )
			{
				text("Answer '!' to get a shell. Type !man to view the "
				     "upgrade(7) manual page. You can edit the upgrade.conf(5) "
				     "configuration file of the target system to change which "
				     "upgrade operations are performed.\n");
				text("Alternatively, you can answer 'poweroff', 'reboot', or "
				     "'halt' or cancel the upgrade.\n");
				continue;
			}
			else if ( !strcasecmp(input, "exit") )
				exit(0);
			else if ( !strcasecmp(input, "poweroff") )
				exit_gui(0);
			else if ( !strcasecmp(input, "reboot") )
				exit_gui(1);
			else if ( !strcasecmp(input, "halt") )
				exit_gui(2);
			else if ( !strcasecmp(input, "!") )
				break;
			else
				continue;
		}
		if ( !strcasecmp(input, "yes") )
			break;
	}
	text("\n");

	// TODO: Switch to local time zone of the existing system?

	text("Upgrading to " BRAND_DISTRIBUTION_NAME " " VERSIONSTR " now:\n");

	pid_t upgrade_pid = fork();
	if ( upgrade_pid < 0 )
		err(2, "fork");
	if ( upgrade_pid == 0 )
	{
		umask(0022);
		if ( conf.system )
			upgrade_prepare(target_release, &new_release, "", ".");
		install_manifests_detect("", ".", conf.system, conf.ports, conf.ports);
		if ( has_manifest("src") )
		{
			if ( conf.newsrc )
			{
				bool has_src = access_or_die("src", F_OK) == 0;
				if ( has_src )
				{
					preserve_src("newsrc");
					if ( rename("src", "src.tmp") < 0 )
					{
						warn("rename: /src -> /src.tmp");
						_exit(2);
					}
				}
				install_manifest("src", "", ".", (const char*[]){}, 0);
				if ( has_src )
				{
					if ( rename("src", "newsrc") < 0 )
					{
						warn("rename: /src -> /newsrc");
						_exit(2);
					}
					if ( rename("src.tmp", "src") < 0 )
					{
						warn("rename: /src.tmp -> /src");
						_exit(2);
					}
				}
			}
			else if ( conf.src )
			{
				preserve_src("src");
				install_manifest("src", "", ".", (const char*[]){}, 0);
			}
		}
		if ( conf.system )
			upgrade_finalize(target_release, &new_release, "", ".");
		// TODO: Update /etc/upgrade.conf with new release values.
		if ( conf.system )
		{
			printf(" - Creating initrd...\n");
			execute((const char*[]) { "update-initrd", "--sysroot", fs, NULL }, "_e");
		}
		if ( do_upgrade_bootloader )
		{
			printf(" - Installing bootloader...\n");
			execute((const char*[]) { "chroot", "-d", ".", "grub-install",
			        bootloader_dev_path, NULL },
			        "_eqQ");
			printf(" - Configuring bootloader...\n");
			execute((const char*[]) { "chroot", "-d", ".", "update-grub", NULL },
			        "_eqQ");
		}
		else if ( conf.system &&
		          access_or_die("etc/default/grub.d/10_sortix", F_OK) == 0 )
		{
			// Help dual booters by making /etc/default/grub.d/10_sortix.cache.
			printf(" - Creating bootloader fragment...\n");
			execute((const char*[]) { "chroot", "-d", ".",
			                          "/etc/grub.d/default/10_sortix", NULL },
			                          "_eq");
		}
		printf(" - Finishing upgrade...\n");
		_exit(0);
	}
	int upgrade_code;
	waitpid(upgrade_pid, &upgrade_code, 0);
	if ( WIFEXITED(upgrade_code) && WEXITSTATUS(upgrade_code) == 0 )
	{
	}
	else if ( WIFEXITED(upgrade_code) )
		errx(2, "upgrade failed with exit status %i", WEXITSTATUS(upgrade_code));
	else if ( WIFSIGNALED(upgrade_code) )
		errx(2, "upgrade failed: %s", strsignal(WTERMSIG(upgrade_code)));
	else
		errx(2, "upgrade failed: unknown waitpid code %i", upgrade_code);
	text("\n");

	if ( conf.system )
		textf("The %s installation has now been upgraded to %s.\n\n",
		      bdev_path, new_release.pretty_name);
	else if ( conf.newsrc )
		textf("The %s installation now contains the new source code in /newsrc. "
		      "You need to build it as described in development(7).\n\n",
		      bdev_path);
	else if ( conf.src )
		textf("The %s installation now contains the new source code in /src. "
		      "You need to build it as described in development(7).\n\n",
		      bdev_path);
	else
		textf("The %s installation has been upgraded to %s as requested.\n\n",
		      bdev_path, new_release.pretty_name);

	if ( target_release->abi_major < new_release.abi_major )
	{
		text("Note: The system has been upgraded across a major ABI change. "
		     "Locally compiled programs must be recompiled as they no longer "
		     "can be expected to work.\n\n");
	}
	else if ( target_release->abi_major == new_release.abi_major &&
	          target_release->abi_minor < new_release.abi_minor )
	{
		text("Note: The system has been upgraded across a minor ABI change.\n\n");
	}
	else if ( new_release.abi_major < target_release->abi_major ||
	          (target_release->abi_major == new_release.abi_major &&
	           new_release.abi_minor < target_release->abi_minor) )
	{
		text("Note: The system has been downgraded to an earlier ABI. "
		     "Locally compiled programs must be recompiled as they no longer "
		     "can be expected to work.\n\n");
	}

	while ( true )
	{
		prompt(input, sizeof(input), "finally",
		       "What now? (exit/poweroff/reboot/halt)", "reboot");
		if ( !strcasecmp(input, "exit") )
			exit(0);
		else if ( !strcasecmp(input, "poweroff") )
			exit_gui(0);
		else if ( !strcasecmp(input, "reboot") )
			exit_gui(1);
		else if ( !strcasecmp(input, "halt") )
			exit_gui(2);
	}
}
