/* * iso.cc - Implementation of the CD image Horizon Image Creation backend * image, the image processing utilities for * Project Horizon * * Copyright (c) 2020 Adélie Linux and contributors. All rights reserved. * This code is licensed under the AGPL 3.0 license, as noted in the * LICENSE-code file in the root directory of this repository. * * SPDX-License-Identifier: AGPL-3.0-only */ #include /* getenv */ #include /* strlen, strtok */ #include /* ifstream, ofstream */ #include #include #include "basic.hh" #include "hscript/util.hh" #include "util/filesystem.hh" #include "util/output.hh" using namespace boost::algorithm; const std::vector data_dirs() { std::vector dirs; char *home = std::getenv("XDG_DATA_HOME"); if(home != nullptr && std::strlen(home) > 0) { dirs.push_back(home); } else { home = std::getenv("HOME"); if(home != nullptr && std::strlen(home) > 0) { dirs.push_back(std::string(home) + "/.local/share"); } else { home = std::getenv("APPDATA"); if(home != nullptr) { dirs.push_back(home); } else { /* Give up. */ } } } const char *sys_c = std::getenv("XDG_DATA_DIRS"); if(sys_c == nullptr || std::strlen(sys_c) == 0) { sys_c = "/usr/local/share:/usr/share"; } const std::string sys{sys_c}; std::vector temp; boost::split(temp, sys, is_any_of(":")); std::move(temp.begin(), temp.end(), std::back_inserter(dirs)); return dirs; } const fs::path find_data_file(const std::string &name) { error_code ec; for(const auto &p : data_dirs()) { fs::path src = fs::path(p).append("horizon").append("iso").append(name); if(fs::exists(src, ec)) { return src; } } return fs::path(); } bool copy_volume_icon_to(fs::path ir_dir) { error_code ec; const fs::path dest = ir_dir.append("cdroot").append("VolumeIcon.icns"); const fs::path src = find_data_file("VolumeIcon.icns"); /* No volume icon. */ if(!src.has_filename()) return false; fs::copy(src, dest, ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not copy volume icon", ec.message()); return false; } return true; } bool write_etc_mtab_to(fs::path target) { std::ofstream mtab(target.append("etc/conf.d/mtab").string()); if(!mtab) { output_error("CD backend", "failed to open mtab configuration"); return false; } mtab << "mtab_is_file=no" << std::endl; if(mtab.fail() || mtab.bad()) { output_error("CD backend", "failed to write mtab configuration"); return false; } mtab.flush(); mtab.close(); return true; } bool write_fstab_to(fs::path target) { std::ofstream fstab{target.append("etc/fstab").string()}; if(!fstab) { output_error("CD backend", "failed to open fstab"); return false; } fstab << "# Welcome to Adélie Linux." << std::endl << "# This fstab(5) is for the live media only. " << "Do not edit or use for your installation." << std::endl << std::endl << "tmpfs /tmp tmpfs defaults 0 1" << std::endl << "proc /proc proc defaults 0 1" << std::endl; if(fstab.fail() || fstab.bad()) { output_error("CD backend", "failed to write fstab"); return false; } fstab.flush(); fstab.close(); return true; } bool write_etc_issue_to(fs::path target) { error_code ec; const fs::path dest{target.append("etc/issue")}; const fs::path src{find_data_file("issue")}; if(src.has_filename()) { fs::copy(src, dest, ec); return !ec; } /* We don't have a file, so write out our default. */ std::ofstream issue(dest.string()); if(!issue) { output_error("CD backend", "failed to open issue file"); return false; } issue << "Welcome to Adélie Linux!" << std::endl << "You may log in as 'root' to install, or 'live' to play around." << std::endl << "The default root password is 'live' (without quotes)." << std::endl << std::endl << "Have fun." << std::endl; if(issue.fail() || issue.bad()) { output_error("CD backend", "failed to write issue file"); return false; } issue.flush(); issue.close(); return true; } namespace Horizon { namespace Image { class CDBackend : public BasicBackend { public: enum CDError { COMMAND_MISSING = 1, FS_ERROR, COMMAND_ERROR }; explicit CDBackend(const std::string &ir, const std::string &out, const std::map &opts) : BasicBackend(ir, out, opts) {}; int prepare() override { error_code ec; output_info("CD backend", "probing SquashFS tools version..."); if(run_command("mksquashfs", {"-version"}) != 0) { output_error("CD backend", "SquashFS tools are not present"); return COMMAND_MISSING; } output_info("CD backend", "probing xorriso version..."); if(run_command("xorriso", {"-version"}) != 0) { output_error("CD backend", "xorriso is not present"); return COMMAND_MISSING; } output_info("CD backend", "probing grub version..."); if(run_command("grub-mkimage", {"--version"}) != 0) { output_error("CD backend", "grub-mkimage is not present"); return COMMAND_MISSING; } /* REQ: ISO.1 */ if(fs::exists(this->ir_dir, ec) && opts.find("keep") == opts.end()) { output_info("CD backend", "removing old IR tree", this->ir_dir); /* try to umount first, just in case * We don't care if the call fails. */ for(const std::string mount : {"dev", "proc", "sys"}) { const std::string path = this->ir_dir + "/target/" + mount; if(::umount(path.c_str()) == -1) { /* if EBUSY, it will remain mounted when we fs::remove_all * which will affect the host's environment. Bail out. */ if(errno == EBUSY && run_command("umount", {"-R", path}) != 0) { output_error("CD backend", "could not umount " + path); return FS_ERROR; } } } fs::remove_all(this->ir_dir, ec); if(ec) { output_warning("CD backend", "could not remove IR tree", ec.message()); /* we can _try_ to proceed anyway... */ } } output_info("CD backend", "creating directory tree"); /* REQ: ISO.2 */ fs::create_directory(this->ir_dir, ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not create IR directory", ec.message()); return FS_ERROR; } /* REQ: ISO.2 */ fs::create_directory(this->ir_dir + "/cdroot", ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not create ISO directory", ec.message()); return FS_ERROR; } /* REQ: ISO.2 */ fs::create_directory(this->ir_dir + "/target", ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not create target directory", ec.message()); return FS_ERROR; } /* REQ: ISO.4 */ fs::create_directories(this->ir_dir + "/target/etc/default", ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not create target config dir", ec.message()); return FS_ERROR; } /* REQ: ISO.4 */ output_info("CD backend", "configuring boot loader"); std::ofstream grub(this->ir_dir + "/target/etc/default/grub"); grub << "ADELIE_MANUAL_CONFIG=1" << std::endl; if(grub.fail() || grub.bad()) { output_error("CD backend", "failed to configure GRUB"); return FS_ERROR; } grub.close(); return 0; } int create() override { error_code ec; std::string my_arch; std::ifstream archstream(this->ir_dir + "/target/etc/apk/arch"); const std::string target = this->ir_dir + "/target"; const std::string cdpath = this->ir_dir + "/cdroot"; archstream >> my_arch; fs::current_path(this->ir_dir); /* REQ: ISO.7 */ output_info("CD backend", "creating live environment directories"); fs::create_directory(target + "/target", ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not create directory", ec.message()); } fs::create_directories(target + "/media/live", ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not create directory", ec.message()); } /* REQ: ISO.9 */ output_info("CD backend", "configuring mtab"); write_etc_mtab_to(target); /* REQ: ISO.10 */ output_info("CD backend", "enabling required services"); const std::string targetsi = target + "/etc/runlevels/sysinit/"; for(const std::string svc : {"udev", "udev-trigger", "lvmetad"}) { fs::create_symlink("/etc/init.d/" + svc, targetsi + svc, ec); if(ec && ec.value() != EEXIST) { output_error("CD backend", "could not enable service " + svc, ec.message()); return FS_ERROR; } } /* REQ: ISO.12 */ output_info("CD backend", "creating live environment /etc/fstab"); if(!write_fstab_to(target)) return FS_ERROR; /* REQ: ISO.15 */ output_info("CD backend", "configuring login services"); run_command("sed", {"-i", "s/pam_unix.so$/pam_unix.so nullok_secure/", target + "/etc/pam.d/base-auth"}); run_command("chroot", {target, "/usr/bin/passwd", "-d", "live"}); /* REQ: ISO.19 */ output_info("CD backend", "creating live /etc/issue"); if(opts.find("issue-path") != opts.end() && fs::exists(opts.at("issue-path"))) { fs::path dest = fs::path(cdpath).append("etc/issue"); fs::copy(opts.at("issue-path"), dest, ec); if(ec) output_error("CD backend", "could not copy /etc/issue", ec.message()); } else if(!write_etc_issue_to(target)) return FS_ERROR; /* REQ: ISO.22 */ output_info("CD backend", "generating file list"); { std::ofstream exclude(this->ir_dir + "/exclude.list"); exclude << "dev/*" << std::endl << "proc/*" << std::endl << "sys/*" << std::endl; if(exclude.fail() || exclude.bad()) { output_error("CD backend", "failed to write exclusion list"); return FS_ERROR; } exclude.flush(); exclude.close(); } /* REQ: ISO.22 */ output_info("CD backend", "creating SquashFS"); const std::string squashpath = cdpath + "/" + my_arch + ".squashfs"; if(run_command("mksquashfs", {target, squashpath, "-noappend", "-wildcards", "-ef", this->ir_dir + "/exclude.list"}) != 0) { output_error("CD backend", "failed to create SquashFS"); return COMMAND_ERROR; } /* REQ: ISO.21 */ if(opts.find("icon-path") != opts.end() && fs::exists(opts.at("icon-path"))) { fs::path dest = fs::path(cdpath).append("VolumeIcon.icns"); fs::copy(opts.at("icon-path"), dest, ec); if(ec) output_error("CD backend", "could not copy volume icon", ec.message()); } else if(!copy_volume_icon_to(this->ir_dir)) { output_warning("CD backend", "No volume icon could be found."); } /* REQ: ISO.23 */ output_info("CD backend", "creating initrd"); std::string kver, kverpath; for(const auto &dent : fs::recursive_directory_iterator(target + "/usr/share/kernel", ec)) { if(fs::is_regular_file(dent.path()) && dent.path().filename().string() == "kernel.release") { kverpath = dent.path().string(); break; } } if(kverpath.length() == 0) { output_error("CD backend", "cannot find kernel.release"); return FS_ERROR; } std::ifstream kverstream(kverpath); kverstream >> kver; const std::string irdname = "initrd-" + my_arch; if(run_command("chroot", {target, "dracut", "--kver", kver, "-N", "--force", "-a", "dmsquash-live", "/boot/" + irdname}) != 0) { output_error("CD backend", "dracut failed to create initramfs"); return COMMAND_ERROR; } fs::rename(target + "/boot/" + irdname, cdpath + "/" + irdname, ec); if(ec) { output_error("CD backend", "cannot install initrd to CD root"); return FS_ERROR; } /* REQ: ISO.24 */ std::string postscript; if(opts.find("post-script") != opts.end() && fs::exists(opts.at("post-script"))) { postscript = opts.at("post-script"); } else { for(const auto &path : data_dirs()) { fs::path candidate{fs::path{path}.append("horizon") .append("iso").append("post-" + my_arch + ".sh")}; if(fs::exists(candidate, ec)) { postscript = candidate.string(); break; } } } if(postscript.length() > 0) { output_info("CD backend", "running architecture-specific script", postscript); if(run_command("/bin/sh", {"-e", postscript}) != 0) { output_error("CD backend", "architecture-specific script failed"); return COMMAND_ERROR; } } /* REQ: ISO.25 */ output_info("CD backend", "installing kernel"); for(const auto &candidate : fs::directory_iterator(target + "/boot")) { auto name = candidate.path().filename().string(); if(name.length() > 6 && name.substr(0, 6) == "vmlinu") { fs::copy(candidate.path(), cdpath + "/kernel-" + my_arch, ec); if(ec) { output_error("CD backend", "failed to copy kernel", ec.message()); return FS_ERROR; } fs::remove(candidate.path(), ec); break; } } /* REQ: ISO.26 */ output_info("CD backend", "creating ISO"); std::vector iso_args = {"-as", "mkisofs", "-o", out_path, "-joliet", "-rational-rock", "-V", "Adelie-"+my_arch}; std::vector arch_args; std::string raw_arch; { const fs::path param_file = find_data_file("iso-params-" + my_arch); if(param_file.has_filename()) { std::ifstream params{param_file.string()}; if(!params) { output_warning("CD backend", "couldn't read ISO params"); } else { std::getline(params, raw_arch); } } } if(raw_arch.length() > 0) { boost::split(arch_args, raw_arch, is_any_of(" ")); std::move(arch_args.begin(), arch_args.end(), std::back_inserter(iso_args)); } if(opts.find("iso-params") != opts.end()) { boost::split(arch_args, opts.at("iso-params"), is_any_of(" ")); std::move(arch_args.begin(), arch_args.end(), std::back_inserter(iso_args)); } iso_args.push_back(cdpath); return run_command("xorriso", iso_args); } int finalise() override { output_info("CD backend", "Live image created successfully", out_path); return 0; } }; __attribute__((constructor(400))) void register_cd_backend() { BackendManager::register_backend( {"iso", "Create a CD image (.iso)", [](const std::string &ir_dir, const std::string &out_path, const std::map &opts) { return new CDBackend(ir_dir, out_path, opts); } }); } } }