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