From a4e928e3ed0295d29940103dc690e202ed110bc0 Mon Sep 17 00:00:00 2001 From: Louis Bettens Date: Mon, 8 Aug 2022 16:34:05 +0200 Subject: [PATCH] tool: rewrite in C++ --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .gitignore | 3 + Taskfile.yaml | 37 +++++ checks/imperative-management.nix | 5 +- nix/devshell.toml | 38 +++-- nix/set-boost-root.nix | 8 + tool/.clang-format | 5 + tool/.clang-tidy | 0 tool/common.cpp | 53 +++++++ tool/common.hpp | 45 ++++++ tool/create.bash | 36 ----- tool/create.cpp | 109 +++++++++++++ tool/create_arg.bash | 5 - tool/functions.bash | 60 -------- tool/install.bash | 29 ---- tool/install.cpp | 99 ++++++++++++ tool/install_arg.bash | 21 --- tool/main.bash | 45 ------ tool/main.cpp | 79 ++++++++++ tool/main_arg.bash | 23 --- tool/manifest.hpp | 257 +++++++++++++++++++++++++++++++ tool/meson.build | 34 ++++ tool/package.nix | 36 +---- tool/remove.bash | 7 - tool/remove.cpp | 71 +++++++++ tool/remove_arg.bash | 2 - tool/template.bash | 5 - tool/template_arg.bash | 2 - tool/upgrade.bash | 5 - tool/upgrade.cpp | 86 +++++++++++ tool/upgrade_arg.bash | 2 - 31 files changed, 926 insertions(+), 283 deletions(-) create mode 100644 Taskfile.yaml create mode 100644 nix/set-boost-root.nix create mode 100644 tool/.clang-format create mode 100644 tool/.clang-tidy create mode 100644 tool/common.cpp create mode 100644 tool/common.hpp delete mode 100644 tool/create.bash create mode 100644 tool/create.cpp delete mode 100644 tool/create_arg.bash delete mode 100644 tool/functions.bash delete mode 100644 tool/install.bash create mode 100644 tool/install.cpp delete mode 100644 tool/install_arg.bash delete mode 100644 tool/main.bash create mode 100644 tool/main.cpp delete mode 100644 tool/main_arg.bash create mode 100644 tool/manifest.hpp create mode 100644 tool/meson.build delete mode 100644 tool/remove.bash create mode 100644 tool/remove.cpp delete mode 100644 tool/remove_arg.bash delete mode 100644 tool/template.bash delete mode 100644 tool/template_arg.bash delete mode 100644 tool/upgrade.bash create mode 100644 tool/upgrade.cpp delete mode 100644 tool/upgrade_arg.bash diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9d36e47..bb986dd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -13,4 +13,4 @@ - [ ] Documented intended functionality - [ ] Added checks for intended functionality - [ ] Changed Nix files are formatted per `nixpkgs-fmt` -- [ ] Changed Bash files are formatted per `shfmt` +- [ ] Changed C++ files are formatted per `clang-format` diff --git a/.gitignore b/.gitignore index 0ee877f..fe4c68a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ result* /template/flake.lock + +build/ +meson-*/ diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..e66d81f --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,37 @@ +# https://taskfile.dev + +version: '3' + +tasks: + default: + deps: [build] + + setup: + cmds: + - meson setup tool/ build/ + status: + - test -d build/ + + build: + deps: [setup] + dir: build/ + cmds: + - meson compile + + fmt: + deps: [fmt/nix, fmt/cpp] + fmt/nix: + - nixpkgs-fmt . + fmt/cpp: + deps: [setup] + cmds: + - ninja -C build/ clang-format + + lint: + deps: [lint/cpp] + lint/cpp: + - ninja -C build/ clang-tidy + - iwyu_tool.py -p=build/ + + clean: + - git clean -Xdf build/ diff --git a/checks/imperative-management.nix b/checks/imperative-management.nix index 3ff435c..bdcec6c 100644 --- a/checks/imperative-management.nix +++ b/checks/imperative-management.nix @@ -29,8 +29,7 @@ let inherit name; nodes.machine = { environment.systemPackages = [ - # wrapper clears PATH to check for implicit dependencies - (writeShellScriptBin "miniguest" ''PATH= exec ${miniguest}/bin/miniguest "$@"'') + miniguest ]; system.extraDependencies = [ pinned-nixpkgs (import pinned-nixpkgs { inherit system; }).stdenvNoCC ]; virtualisation.memorySize = 1024; @@ -64,7 +63,7 @@ lib.optionalAttrs stdenv.isLinux { name = "miniguest-install-rename"; testScript = '' machine.succeed(""" - miniguest install --name=renamed_dummy /tmp/flake1#dummy + miniguest install --name renamed_dummy /tmp/flake1#dummy """) assert "foo" in machine.succeed(""" cat /etc/miniguests/renamed_dummy/boot/init diff --git a/nix/devshell.toml b/nix/devshell.toml index bd5a425..918dfb8 100644 --- a/nix/devshell.toml +++ b/nix/devshell.toml @@ -1,30 +1,50 @@ -imports = [ "git.hooks" ] +imports = [ + "git.hooks", + "language.c", + "set-boost-root.nix", +] [devshell] name = "miniguest" - -[[commands]] -package = "bash" +packages = [ + "ninja", # for meson +] [[commands]] package = "nixFlakes" +[[commands]] +name = "task" +package = "go-task" + +[[commands]] +package = "meson" + [[commands]] package = "nixpkgs-fmt" category = "formatters" [[commands]] -package = "shfmt" +name = "clang-format" +package = "clang-tools" category = "formatters" [[commands]] -package = "argbash" -category = "preprocessors" +name = "clang-tidy" +package = "clang-tools" +category = "linters" [[commands]] -package = "shellcheck" +package = "include-what-you-use" category = "linters" +[language.c] +includes = [ + "boehmgc", + "nixFlakes", + "nlohmann_json", +] + [git.hooks] enable = true pre-commit.text = """ @@ -41,5 +61,5 @@ function find_staged { } find_staged '*.nix' | xargs -r nixpkgs-fmt --check || exit -find_staged '*.bash' | xargs -r shfmt -d -s || exit +find_staged '*.hpp' '*.cpp' | xargs -r clang-format -n -Werror || exit """ diff --git a/nix/set-boost-root.nix b/nix/set-boost-root.nix new file mode 100644 index 0000000..1849719 --- /dev/null +++ b/nix/set-boost-root.nix @@ -0,0 +1,8 @@ +{ pkgs, ... }: + +{ + env = [{ + name = "BOOST_ROOT"; + value = pkgs.boost.dev; + }]; +} diff --git a/tool/.clang-format b/tool/.clang-format new file mode 100644 index 0000000..7909354 --- /dev/null +++ b/tool/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: LLVM + +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortBlocksOnASingleLine: Empty diff --git a/tool/.clang-tidy b/tool/.clang-tidy new file mode 100644 index 0000000..e69de29 diff --git a/tool/common.cpp b/tool/common.cpp new file mode 100644 index 0000000..fbac870 --- /dev/null +++ b/tool/common.cpp @@ -0,0 +1,53 @@ +/* Copyright 2022 Louis Bettens + * + * This file is part of the Miniguest companion. + * + * The Miniguest companion 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 3 of the License, or (at your + * option) any later version. + * + * Miniguest 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 Miniguest. If not, see . + */ + +#include "common.hpp" + +#include "error.hh" + +namespace fs = std::filesystem; +using namespace nix; + +Context ContextBuilder::build() { + if (!symlink_path) + fs::create_directory(default_symlinks_dir); + if (!profile_path) + fs::create_directory(default_profiles_dir); + return {symlink_path.value_or(default_symlinks_dir / guest_name), + profile_path.value_or(default_profiles_dir / guest_name)}; +} + +void Context::ensure_symlink() { + auto st = fs::symlink_status(symlink_path); + if (!fs::exists(st)) + fs::create_symlink(profile_path, symlink_path); + else + check_symlink(st); +} +void Context::check_symlink(const fs::file_status &st) { + if (!fs::is_symlink(st) || fs::read_symlink(symlink_path) != profile_path) + throw Error(1, + "not touching symlink because it's not in an expected state"); +} +void Context::remove_symlink() { + auto st = fs::symlink_status(symlink_path); + if (fs::exists(st)) + check_symlink(st); + + fs::remove(symlink_path); +} diff --git a/tool/common.hpp b/tool/common.hpp new file mode 100644 index 0000000..024db05 --- /dev/null +++ b/tool/common.hpp @@ -0,0 +1,45 @@ +/* Copyright 2022 Louis Bettens + * + * This file is part of the Miniguest companion. + * + * The Miniguest companion 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 3 of the License, or (at your + * option) any later version. + * + * Miniguest 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 Miniguest. If not, see . + */ + +#include +#include + +struct ContextBuilder final { + std::string guest_name; + std::optional symlink_path, profile_path; + std::filesystem::path default_symlinks_dir = "/etc/miniguests", + default_profiles_dir = + "/nix/var/nix/profiles/miniguest-profiles"; + + struct Context build(); +}; + +struct Context final { + const std::filesystem::path symlink_path, profile_path; + + void ensure_symlink(); + void remove_symlink(); + +private: + Context(std::filesystem::path symlink_path, + std::filesystem::path profile_path) + : symlink_path(symlink_path), profile_path(profile_path) {} + friend Context ContextBuilder::build(); + + void check_symlink(const std::filesystem::file_status &st); +}; diff --git a/tool/create.bash b/tool/create.bash deleted file mode 100644 index 0236ed2..0000000 --- a/tool/create.bash +++ /dev/null @@ -1,36 +0,0 @@ -source create_arg.bash - -name="$_arg_guest_name" - -set_color_red=$'\e[1m\e[31m' -reset_color=$'\e(B\e[m' - -case "${_arg_hypervisor:?}" in -libvirt) - cat <. + */ + +#include "common.hpp" +#include "manifest.hpp" + +#include "build-result.hh" +#include "command.hh" +#include "derived-path.hh" +#include "eval-cache.hh" + +#include +#include + +using namespace nix; +namespace fs = std::filesystem; + +struct CmdCreate : virtual EvalCommand, virtual MixProfile { + std::string guest_name; + std::optional hypervisor; + + const std::string set_color_red = "\e[1m\e[31m", reset_color = "\e(B\e[m"; + + CmdCreate() { + expectArg("name", &guest_name); + addFlag({ + .longName = "type", + .shortName = 't', + .description = "hypervisor to configure (default: libvirt)", + .labels = {"hypervisor"}, + .handler = {&hypervisor}, + }); + } + + std::string description() override { + return "generate a command to configure the guest in the hypervisor"; + } + + std::string doc() override { + return R""(miniguest create [-h|--help] [-t|--hypervisor ] + : name of guest to create + -h, --help: Prints help + -t, --hypervisor: hypervisor to configure. Can be one of: 'libvirt' and 'lxc' (default: 'libvirt'))""; + } + + std::vector prepare_command() { + if (*hypervisor == "libvirt") + return { + "virt-install -n " + guest_name, + "--connect qemu:///system", + "--os-variant nixos-unstable", + "--memory 1536", + "--disk none", + "--import", + "--boot kernel=/etc/miniguests/" + guest_name + + "/kernel,initrd=/etc/miniguests/" + guest_name + "/initrd", + "--filesystem /nix/store/,nix-store,readonly=yes,accessmode=squash", + "--filesystem /etc/miniguests/" + guest_name + + "/boot/,boot,readonly=yes,accessmode=squash", + }; + + else if (*hypervisor == "lxc") + return { + "lxc-create " + guest_name, + "-f extra-config", + "-t local --", + "-m @lxc_template@/meta.tar.xz", + "-f @lxc_template@/rootfs.tar.xz", + }; + else + throw Error(2, "unknown hypervisor type"); + } + + void run(ref store) override { + if (!hypervisor) + hypervisor = "libvirt"; + + auto cmd = prepare_command(); + + if (*hypervisor == "lxc") + std::cout << "# " << set_color_red + << "WARNING: make sure root is uid-mapped, otherwise you might " + "experience store corruption in the host!" + << reset_color << std::endl; + std::cout << "# Create " << guest_name << " using:" << std::endl; + for (auto &line : cmd) + std::cout << line << " \\\n "; + std::cout << std::endl; + } + + virtual ~CmdCreate() = default; +}; + +static auto rCmdCreate = registerCommand("create"); diff --git a/tool/create_arg.bash b/tool/create_arg.bash deleted file mode 100644 index 377e78d..0000000 --- a/tool/create_arg.bash +++ /dev/null @@ -1,5 +0,0 @@ -# ARG_POSITIONAL_SINGLE(guest-name, name of guest to create) -# ARG_HELP(assist in creatig the guest by configuring the hypervisor) -# ARG_OPTIONAL_SINGLE(hypervisor, t, hypervisor to configure, libvirt) -# ARG_TYPE_GROUP_SET(hypervisors, HYPERVISOR, hypervisor, [libvirt,lxc]) -# ARGBASH_GO diff --git a/tool/functions.bash b/tool/functions.bash deleted file mode 100644 index 8f0c5b1..0000000 --- a/tool/functions.bash +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2021 Louis Bettens -# -# This file is part of the Miniguest companion. -# -# The Miniguest companion 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 3 of the License, or (at your -# option) any later version. -# -# Miniguest 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 Miniguest. If not, see . - -function run_nix { - command "$nix" --experimental-features "nix-command flakes ca-references" "$@" -} - -flake= -guest_name= - -function parse_flake_reference { - [[ $1 =~ ^(.*)\#([^\#\"]*)$ ]] || die "cannot parse flake reference" - flake="${BASH_REMATCH[1]}" - guest_name="${BASH_REMATCH[2]}" -} - -function install_profile { - [[ $# -eq 2 ]] || die "$FUNCNAME: wrong number of arguments!" - local guest_name="$1" - local target="$2" - run_nix profile install --profile "$profiles_dir/$guest_name" "$target" || - die "unable to install $guest_name!" $? -} - -function upgrade_profile { - [[ $# -eq 1 ]] || die "$FUNCNAME: wrong number of arguments!" - local guest_name="$1" - run_nix profile upgrade --profile "$profiles_dir/$guest_name" || - die "unable to upgrade $guest_name!" $? -} - -function reset_profile { - [[ $# -eq 1 ]] || die "$FUNCNAME: wrong number of arguments!" - local guest_name="$1" - run_nix profile remove --profile "$profiles_dir/$guest_name" '.*' || - die "unable to remove guest!" $? -} - -function have_control_of_symlink { - [[ $# -eq 1 ]] || die "$FUNCNAME: wrong number of arguments!" - local symlink="$guests_dir/$guest_name" - [[ ! -e $symlink || -L $symlink && $(readlink $symlink) -ef "$profiles_dir/$guest_name" ]] || { - echo >&2 "not touching $guests_dir/$guest_name because it's not in an expected state" - false - } -} diff --git a/tool/install.bash b/tool/install.bash deleted file mode 100644 index 3cdfa67..0000000 --- a/tool/install.bash +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2021 Louis Bettens -# -# This file is part of the Miniguest companion. -# -# The Miniguest companion 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 3 of the License, or (at your -# option) any later version. -# -# Miniguest 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 Miniguest. If not, see . - -source install_arg.bash - -parse_flake_reference "$_arg_flake_reference" -profile_name="${_arg_name:-$guest_name}" - -mkdir -p "$guests_dir" || die "" $? -mkdir -p "$profiles_dir" || die "" $? - -reset_profile "$profile_name" # FIXME: need an atomic reset-and-install -install_profile "$profile_name" "$flake#nixosConfigurations.$guest_name.config.system.build.miniguest" - -have_control_of_symlink "$profile_name" && ln -sf "$profiles_dir/$profile_name" "$guests_dir" diff --git a/tool/install.cpp b/tool/install.cpp new file mode 100644 index 0000000..fb32b6a --- /dev/null +++ b/tool/install.cpp @@ -0,0 +1,99 @@ +/* Copyright 2022 Louis Bettens + * + * This file is part of the Miniguest companion. + * + * The Miniguest companion 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 3 of the License, or (at your + * option) any later version. + * + * Miniguest 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 Miniguest. If not, see . + */ + +#include "common.hpp" +#include "manifest.hpp" + +#include "build-result.hh" +#include "command.hh" +#include "derived-path.hh" +#include "eval-cache.hh" + +#include +#include + +using namespace nix; +namespace fs = std::filesystem; + +struct CmdInstall : virtual InstallableCommand, virtual MixProfile { + std::optional guest_name; + + CmdInstall() { + addFlag({ + .longName = "name", + .shortName = 'n', + .description = "Name of the guest (default: attribute name)", + .labels = {"name"}, + .handler = {&guest_name}, + }); + } + + std::string description() override { + return "build the guest and install it into a nix profile"; + } + + std::string doc() override { + return R""(miniguest install [-n|--name ] + : guest to build + -n, --name: name of the profile (no default))""; + } + + Strings getDefaultFlakeAttrPaths() override { + return {"nixosConfigurations.default"}; + } + Strings getDefaultFlakeAttrPathPrefixes() override { + return {"nixosConfigurations."}; + } + + void run(ref store) override { + auto evalState = getEvalState(); + + if (!guest_name) + guest_name = installable->getCursor(*evalState)->getAttrPath().back(); + + ContextBuilder bld; + bld.guest_name = *guest_name; + if (profile) + bld.profile_path = *profile; + + Context ctx = bld.build(); + profile = ctx.profile_path.native(); + + auto installableFlake = + std::static_pointer_cast(installable); + for (auto &a : installableFlake->attrPaths) + a += ".config.system.build.miniguest"; + auto result = Installable::build(getEvalStore(), store, Realise::Outputs, + {installable}); + + auto [attrPath, resolvedRef, drv] = installableFlake->toDerivation(); + + ProfileManifest manifest(*getEvalState(), *profile); + ProfileElement element; + element.source = {installableFlake->flakeRef, resolvedRef, attrPath}; + element.updateStorePaths(getEvalStore(), store, result); + manifest.elements = {element}; + updateProfile(manifest.build(store)); + + ctx.ensure_symlink(); + } + + virtual ~CmdInstall() = default; +}; + +static auto rCmdInstall = registerCommand("install"); diff --git a/tool/install_arg.bash b/tool/install_arg.bash deleted file mode 100644 index d08627f..0000000 --- a/tool/install_arg.bash +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2021 Louis Bettens -# -# This file is part of the Miniguest companion. -# -# The Miniguest companion 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 3 of the License, or (at your -# option) any later version. -# -# Miniguest 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 Miniguest. If not, see . - -# ARG_HELP(build the guest and install it into a nix profile) -# ARG_POSITIONAL_SINGLE(flake-reference, guest to build) -# ARG_OPTIONAL_SINGLE(name, n, name of the profile) -# ARGBASH_GO diff --git a/tool/main.bash b/tool/main.bash deleted file mode 100644 index 3ea6b91..0000000 --- a/tool/main.bash +++ /dev/null @@ -1,45 +0,0 @@ -#! @bash@/bin/sh - -# Copyright 2021 Louis Bettens -# -# This file is part of the Miniguest companion. -# -# The Miniguest companion 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 3 of the License, or (at your -# option) any later version. -# -# Miniguest 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 Miniguest. If not, see . - -source main_arg.bash - -guests_dir="$_arg_guests_dir" -profiles_dir="/nix/var/nix/profiles/miniguest-profiles" -nix="$_arg_nix" - -source functions.bash - -set -- "${_arg_leftovers[@]}" # reset parameters to subcommand arguments -case "${_arg_command:?}" in -install) - source install.bash - ;; -upgrade) - source upgrade.bash - ;; -remove) - source remove.bash - ;; -create) - source create.bash - ;; -template) - source template.bash - ;; -esac diff --git a/tool/main.cpp b/tool/main.cpp new file mode 100644 index 0000000..0eb3284 --- /dev/null +++ b/tool/main.cpp @@ -0,0 +1,79 @@ +/* Copyright 2022 Louis Bettens + * + * This file is part of the Miniguest companion. + * + * The Miniguest companion 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 3 of the License, or (at your + * option) any later version. + * + * Miniguest 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 Miniguest. If not, see . + */ + +#include "command.hh" +#include "common-args.hh" +#include "shared.hh" + +using namespace nix; + +struct MiniguestArgs final : virtual MultiCommand, virtual MixCommonArgs { + bool helpRequested = false; + + MiniguestArgs() + : MultiCommand(RegisterCommand::getCommandsFor({})), + MixCommonArgs("miniguest") { + addFlag({ + .longName = "help", + .description = "Show usage information.", + .handler = {[&]() { helpRequested = true; }}, + }); + } + + std::string description() override { + return "Companion tool for Miniguest lightweight NixOS images"; + } + + std::string doc() override { + std::string doc = "Available subcommands:"; + for (auto [name, cmd] : commands) { + auto line = " " + name + ":"; + line.resize(16, ' '); + line += cmd()->description(); + doc.push_back('\n'); + doc.append(line); + } + return doc; + } +}; + +void main0(int argc, char **argv) { + initNix(); + initGC(); + MiniguestArgs args; + + settings.experimentalFeatures = {Xp::Flakes}; + + args.parseCmdline(argvToStrings(argc, argv)); + if (args.helpRequested) { + Args &cmd = args.command ? static_cast(*args.command->second) + : static_cast(args); + std::cout << cmd.description() << std::endl; + std::cout << cmd.doc() << std::endl; + return; + } + if (!args.command) + throw UsageError("no subcommand specified"); + + args.command->second->prepare(); + args.command->second->run(); +} + +int main(int argc, char **argv) { + return nix::handleExceptions(argv[0], [=]() { main0(argc, argv); }); +} diff --git a/tool/main_arg.bash b/tool/main_arg.bash deleted file mode 100644 index dc10907..0000000 --- a/tool/main_arg.bash +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2021 Louis Bettens -# -# This file is part of the Miniguest companion. -# -# The Miniguest companion 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 3 of the License, or (at your -# option) any later version. -# -# Miniguest 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 Miniguest. If not, see . - -# ARG_POSITIONAL_SINGLE(command, subcommand to run) -# ARG_TYPE_GROUP_SET(commands, COMMAND, command, [install,upgrade,remove,create,template,help]) -# ARG_OPTIONAL_SINGLE(guests-dir, , directory containing guests profiles, /etc/miniguests) -# ARG_OPTIONAL_SINGLE(nix, , path to the nix binary, @nixFlakes@/bin/nix) -# ARG_LEFTOVERS(subcommand arguments) -# ARGBASH_GO diff --git a/tool/manifest.hpp b/tool/manifest.hpp new file mode 100644 index 0000000..89eb363 --- /dev/null +++ b/tool/manifest.hpp @@ -0,0 +1,257 @@ +/** Copyright Nix contributors + * Distributed under the GNU LGPL-2.1 license + * + * from Nix 2.8.1 + */ +#include "archive.hh" +#include "builtins/buildenv.hh" +#include "command.hh" +#include "common-args.hh" +#include "derivations.hh" +#include "flake/flakeref.hh" +#include "names.hh" +#include "profiles.hh" +#include "shared.hh" +#include "store-api.hh" + +#include +#include +#include + +// don't reformat copypasted code +// clang-format off + +namespace nix { +// from nix/profile.cc +struct ProfileElementSource +{ + FlakeRef originalRef; + // FIXME: record original attrpath. + FlakeRef resolvedRef; + std::string attrPath; + // FIXME: output names + + bool operator < (const ProfileElementSource & other) const + { + return + std::pair(originalRef.to_string(), attrPath) < + std::pair(other.originalRef.to_string(), other.attrPath); + } +}; + +struct ProfileElement +{ + StorePathSet storePaths; + std::optional source; + bool active = true; + // FIXME: priority + + std::string describe() const + { + if (source) + return fmt("%s#%s", source->originalRef, source->attrPath); + StringSet names; + for (auto & path : storePaths) + names.insert(DrvName(path.name()).name); + return concatStringsSep(", ", names); + } + + std::string versions() const + { + StringSet versions; + for (auto & path : storePaths) + versions.insert(DrvName(path.name()).version); + return showVersions(versions); + } + + bool operator < (const ProfileElement & other) const + { + return std::tuple(describe(), storePaths) < std::tuple(other.describe(), other.storePaths); + } + + void updateStorePaths( + ref evalStore, + ref store, + const BuiltPaths & builtPaths) + { + // FIXME: respect meta.outputsToInstall + storePaths.clear(); + for (auto & buildable : builtPaths) { + std::visit(overloaded { + [&](const BuiltPath::Opaque & bo) { + storePaths.insert(bo.path); + }, + [&](const BuiltPath::Built & bfd) { + for (auto & output : bfd.outputs) + storePaths.insert(output.second); + }, + }, buildable.raw()); + } + } +}; + +struct ProfileManifest +{ + std::vector elements; + + ProfileManifest() { } + + ProfileManifest(EvalState & state, const Path & profile) + { + auto manifestPath = profile + "/manifest.json"; + + if (pathExists(manifestPath)) { + auto json = nlohmann::json::parse(readFile(manifestPath)); + + auto version = json.value("version", 0); + std::string sUrl; + std::string sOriginalUrl; + switch(version){ + case 1: + sUrl = "uri"; + sOriginalUrl = "originalUri"; + break; + case 2: + sUrl = "url"; + sOriginalUrl = "originalUrl"; + break; + default: + throw Error("profile manifest '%s' has unsupported version %d", manifestPath, version); + } + + for (auto & e : json["elements"]) { + ProfileElement element; + for (auto & p : e["storePaths"]) + element.storePaths.insert(state.store->parseStorePath((std::string) p)); + element.active = e["active"]; + if (e.value(sUrl,"") != "") { + element.source = ProfileElementSource{ + parseFlakeRef(e[sOriginalUrl]), + parseFlakeRef(e[sUrl]), + e["attrPath"] + }; + } + elements.emplace_back(std::move(element)); + } + } + + /* + else if (pathExists(profile + "/manifest.nix")) { + // FIXME: needed because of pure mode; ugly. + state.allowPath(state.store->followLinksToStore(profile)); + state.allowPath(state.store->followLinksToStore(profile + "/manifest.nix")); + + auto drvInfos = queryInstalled(state, state.store->followLinksToStore(profile)); + + for (auto & drvInfo : drvInfos) { + ProfileElement element; + element.storePaths = {drvInfo.queryOutPath()}; + elements.emplace_back(std::move(element)); + } + } + */ + } + + std::string toJSON(Store & store) const + { + auto array = nlohmann::json::array(); + for (auto & element : elements) { + auto paths = nlohmann::json::array(); + for (auto & path : element.storePaths) + paths.push_back(store.printStorePath(path)); + nlohmann::json obj; + obj["storePaths"] = paths; + obj["active"] = element.active; + if (element.source) { + obj["originalUrl"] = element.source->originalRef.to_string(); + obj["url"] = element.source->resolvedRef.to_string(); + obj["attrPath"] = element.source->attrPath; + } + array.push_back(obj); + } + nlohmann::json json; + json["version"] = 2; + json["elements"] = array; + return json.dump(); + } + + StorePath build(ref store) + { + auto tempDir = createTempDir(); + + StorePathSet references; + + Packages pkgs; + for (auto & element : elements) { + for (auto & path : element.storePaths) { + if (element.active) + pkgs.emplace_back(store->printStorePath(path), true, 5); + references.insert(path); + } + } + + buildProfile(tempDir, std::move(pkgs)); + + writeFile(tempDir + "/manifest.json", toJSON(*store)); + + /* Add the symlink tree to the store. */ + StringSink sink; + dumpPath(tempDir, sink); + + auto narHash = hashString(htSHA256, sink.s); + + ValidPathInfo info { + store->makeFixedOutputPath(FileIngestionMethod::Recursive, narHash, "profile", references), + narHash, + }; + info.references = std::move(references); + info.narSize = sink.s.size(); + info.ca = FixedOutputHash { .method = FileIngestionMethod::Recursive, .hash = info.narHash }; + + StringSource source(sink.s); + store->addToStore(info, source); + + return std::move(info.path); + } + + static void printDiff(const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent) + { + auto prevElems = prev.elements; + std::sort(prevElems.begin(), prevElems.end()); + + auto curElems = cur.elements; + std::sort(curElems.begin(), curElems.end()); + + auto i = prevElems.begin(); + auto j = curElems.begin(); + + bool changes = false; + + while (i != prevElems.end() || j != curElems.end()) { + if (j != curElems.end() && (i == prevElems.end() || i->describe() > j->describe())) { + std::cout << fmt("%s%s: ∅ -> %s\n", indent, j->describe(), j->versions()); + changes = true; + ++j; + } + else if (i != prevElems.end() && (j == curElems.end() || i->describe() < j->describe())) { + std::cout << fmt("%s%s: %s -> ∅\n", indent, i->describe(), i->versions()); + changes = true; + ++i; + } + else { + auto v1 = i->versions(); + auto v2 = j->versions(); + if (v1 != v2) { + std::cout << fmt("%s%s: %s -> %s\n", indent, i->describe(), v1, v2); + changes = true; + } + ++i; + ++j; + } + } + + if (!changes) + std::cout << fmt("%sNo changes.\n", indent); + } +}; +} diff --git a/tool/meson.build b/tool/meson.build new file mode 100644 index 0000000..ebafba4 --- /dev/null +++ b/tool/meson.build @@ -0,0 +1,34 @@ +project('miniguest', 'cpp', + version : '0.2', + default_options : ['warning_level=1', + 'cpp_std=c++17']) + +add_project_arguments('-DSYSTEM="x86_64-linux"', language: 'cpp') # TODO + + +# Nix dependency +nixlibs = [ + dependency('boost'), # transitive + dependency('nlohmann_json'), # transitive + + dependency('nix-main'), + dependency('nix-expr'), + dependency('nix-cmd'), + dependency('nix-store'), +] +add_project_arguments('-Wno-non-virtual-dtor', language: 'cpp') # present in Nix headers + + +src = [ + 'main.cpp', + 'install.cpp', + 'remove.cpp', + 'upgrade.cpp', + 'create.cpp', + 'common.cpp', +] +exe = executable('miniguest', src, + dependencies : nixlibs, + install : true) + +test('basic', exe) diff --git a/tool/package.nix b/tool/package.nix index 23ad62b..15fd96c 100644 --- a/tool/package.nix +++ b/tool/package.nix @@ -1,4 +1,4 @@ -# Copyright 2021 Louis Bettens +# Copyright 2022 Louis Bettens # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -12,41 +12,21 @@ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -{ lib, stdenvNoCC, argbash, bash, coreutils, miniguest-lxc-template, nixFlakes, shellcheck, makeWrapper }: +{ lib, stdenv, boost, miniguest-lxc-template, meson, ninja, nixFlakes, nlohmann_json, pkg-config }: -stdenvNoCC.mkDerivation { +stdenv.mkDerivation { pname = "miniguest"; - version = "0.1.4"; + version = "0.2"; src = builtins.path { name = "source"; path = ./.; }; - inherit bash nixFlakes; lxc_template = miniguest-lxc-template; - nativeBuildInputs = [ argbash makeWrapper ]; + nativeBuildInputs = [ meson ninja nlohmann_json pkg-config ]; + buildInputs = [ boost nixFlakes ]; - buildPhase = '' - for f in *.bash; do + postPatch = '' + for f in *.cpp; do substituteAllInPlace $f done - for f in *_arg.bash; do - argbash --strip=all -i "$f" - done - ''; - - installPhase = '' - mkdir -p $out/{libexec/miniguest,bin} - mv *.bash $out/libexec/miniguest - chmod +x $out/libexec/miniguest/main.bash - # keep PATH open ended since Nix pulls from the environment e.g. git - makeWrapper $out/libexec/miniguest/main.bash $out/bin/miniguest \ - --prefix PATH ":" "$out/libexec/miniguest:${coreutils}/bin" - ''; - - doInstallCheck = true; - - installCheckInputs = [ shellcheck ]; - - installCheckPhase = '' - shellcheck -e SC2123 -x -s bash $out/bin/miniguest ''; meta = with lib; { diff --git a/tool/remove.bash b/tool/remove.bash deleted file mode 100644 index 11fa5d9..0000000 --- a/tool/remove.bash +++ /dev/null @@ -1,7 +0,0 @@ -source remove_arg.bash - -guest_name="$_arg_guest_name" - -reset_profile "$guest_name" - -have_control_of_symlink "$guest_name" && rm -f "$guests_dir/$guest_name" diff --git a/tool/remove.cpp b/tool/remove.cpp new file mode 100644 index 0000000..5f96e91 --- /dev/null +++ b/tool/remove.cpp @@ -0,0 +1,71 @@ +/* Copyright 2022 Louis Bettens + * + * This file is part of the Miniguest companion. + * + * The Miniguest companion 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 3 of the License, or (at your + * option) any later version. + * + * Miniguest 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 Miniguest. If not, see . + */ + +#include "common.hpp" +#include "manifest.hpp" + +#include "build-result.hh" +#include "command.hh" +#include "derived-path.hh" +#include "eval-cache.hh" + +#include +#include + +using namespace nix; +namespace fs = std::filesystem; + +struct CmdRemove : virtual EvalCommand, virtual MixProfile { + std::string guest_name; + + CmdRemove() { + expectArg("name", &guest_name); + } + + std::string description() override { + return "uninstall a guest"; + } + + std::string doc() override { + return R""(miniguest remove [-h|--help] + : name of guest to remove + -h, --help: Prints help)""; + } + + void run(ref store) override { + ContextBuilder bld; + bld.guest_name = guest_name; + if (profile) + bld.profile_path = *profile; + + Context ctx = bld.build(); + profile = ctx.profile_path.native(); + + // reset profile first + ProfileManifest manifest(*getEvalState(), *profile); + manifest.elements.clear(); + updateProfile(manifest.build(store)); + + // then clean the symlink + ctx.remove_symlink(); + } + + virtual ~CmdRemove() = default; +}; + +static auto rCmdRemove = registerCommand("remove"); diff --git a/tool/remove_arg.bash b/tool/remove_arg.bash deleted file mode 100644 index ad67dc5..0000000 --- a/tool/remove_arg.bash +++ /dev/null @@ -1,2 +0,0 @@ -# ARG_POSITIONAL_SINGLE(guest-name, name of guest to remove) -# ARGBASH_GO diff --git a/tool/template.bash b/tool/template.bash deleted file mode 100644 index 521a370..0000000 --- a/tool/template.bash +++ /dev/null @@ -1,5 +0,0 @@ -source template_arg.bash - -dest_dir="$_arg_dest_dir" - -run_nix flake new -t github:lourkeur/miniguest "$dest_dir" diff --git a/tool/template_arg.bash b/tool/template_arg.bash deleted file mode 100644 index bb3cd56..0000000 --- a/tool/template_arg.bash +++ /dev/null @@ -1,2 +0,0 @@ -# ARG_POSITIONAL_SINGLE(dest-dir, destination directory) -# ARGBASH_GO diff --git a/tool/upgrade.bash b/tool/upgrade.bash deleted file mode 100644 index ffc8d07..0000000 --- a/tool/upgrade.bash +++ /dev/null @@ -1,5 +0,0 @@ -source upgrade_arg.bash - -guest_name="$_arg_guest_name" - -upgrade_profile "$guest_name" diff --git a/tool/upgrade.cpp b/tool/upgrade.cpp new file mode 100644 index 0000000..4fe87f1 --- /dev/null +++ b/tool/upgrade.cpp @@ -0,0 +1,86 @@ +/* Copyright 2022 Louis Bettens + * + * This file is part of the Miniguest companion. + * + * The Miniguest companion 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 3 of the License, or (at your + * option) any later version. + * + * Miniguest 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 Miniguest. If not, see . + */ + +#include "common.hpp" +#include "manifest.hpp" + +#include "build-result.hh" +#include "command.hh" +#include "derived-path.hh" +#include "eval-cache.hh" + +#include +#include + +using namespace nix; +namespace fs = std::filesystem; + +struct CmdUpgrade : virtual EvalCommand, virtual MixProfile { + std::string guest_name; + + CmdUpgrade() { + expectArg("name", &guest_name); + } + + std::string description() override { + return "upgrade a guest through its flake reference"; + } + + std::string doc() override { + return R""(miniguest upgrade [-h|--help] + : name of guest to upgrade + -h, --help: Prints help)""; + } + + void run(ref store) override { + ContextBuilder bld; + bld.guest_name = guest_name; + if (profile) + bld.profile_path = *profile; + + Context ctx = bld.build(); + profile = ctx.profile_path.native(); + + ProfileManifest manifest(*getEvalState(), *profile); + + for (auto &element : manifest.elements) { + if (!element.source || element.source->originalRef.input.isLocked()) + continue; + + auto installable = std::make_shared( + nullptr, getEvalState(), FlakeRef(element.source->originalRef), "", + Strings{element.source->attrPath}, Strings{}, flake::LockFlags{}); + auto [attrPath, resolvedRef, drv] = installable->toDerivation(); + if (element.source->resolvedRef == resolvedRef) + continue; + printInfo("upgrading '%s' from flake '%s' to '%s'", + element.source->attrPath, element.source->resolvedRef, + resolvedRef); + auto result = Installable::build(getEvalStore(), store, Realise::Outputs, + {installable}); + + element.source = {installable->flakeRef, resolvedRef, attrPath}; + element.updateStorePaths(getEvalStore(), store, result); + } + updateProfile(manifest.build(store)); + } + + virtual ~CmdUpgrade() = default; +}; + +static auto rCmdUpgrade = registerCommand("upgrade"); diff --git a/tool/upgrade_arg.bash b/tool/upgrade_arg.bash deleted file mode 100644 index 931e0ea..0000000 --- a/tool/upgrade_arg.bash +++ /dev/null @@ -1,2 +0,0 @@ -# ARG_POSITIONAL_SINGLE(guest-name, name of guest to upgrade) -# ARGBASH_GO