tool: rewrite in C++

This commit is contained in:
Louis Bettens 2022-08-08 16:34:05 +02:00 committed by Louis Bettens
parent dd5c5ef2dc
commit a4e928e3ed
31 changed files with 926 additions and 283 deletions

View File

@ -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`

3
.gitignore vendored
View File

@ -2,3 +2,6 @@
result*
/template/flake.lock
build/
meson-*/

37
Taskfile.yaml Normal file
View File

@ -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/

View File

@ -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

View File

@ -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
"""

8
nix/set-boost-root.nix Normal file
View File

@ -0,0 +1,8 @@
{ pkgs, ... }:
{
env = [{
name = "BOOST_ROOT";
value = pkgs.boost.dev;
}];
}

5
tool/.clang-format Normal file
View File

@ -0,0 +1,5 @@
BasedOnStyle: LLVM
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
AllowShortBlocksOnASingleLine: Empty

0
tool/.clang-tidy Normal file
View File

53
tool/common.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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);
}

45
tool/common.hpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include <filesystem>
#include <optional>
struct ContextBuilder final {
std::string guest_name;
std::optional<std::filesystem::path> 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);
};

View File

@ -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 <<EOF
# Create $name using:
virt-install \\
--connect qemu:///system \\
-n $name --os-variant nixos-unstable \\
--memory 1536 \\
--disk none \\
--import \\
--boot kernel=/etc/miniguests/$name/kernel,initrd=$guests_dir/$name/initrd \\
--filesystem /nix/store/,nix-store,readonly=yes,accessmode=squash \\
--filesystem /etc/miniguests/$name/boot/,boot,readonly=yes,accessmode=squash \\
EOF
;;
lxc)
cat <<EOF
#$set_color_red WARNING: make sure root is uid-mapped, otherwise you might experience store corruption in the host!$reset_color
# Create $name using:
lxc-create $name \\
-f extra-config \\
-t local -- \\
-m @lxc_template@/meta.tar.xz \\
-f @lxc_template@/rootfs.tar.xz \\
EOF
;;
esac

109
tool/create.cpp Normal file
View File

@ -0,0 +1,109 @@
/* 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 <https://www.gnu.org/licenses/>.
*/
#include "common.hpp"
#include "manifest.hpp"
#include "build-result.hh"
#include "command.hh"
#include "derived-path.hh"
#include "eval-cache.hh"
#include <iostream>
#include <vector>
using namespace nix;
namespace fs = std::filesystem;
struct CmdCreate : virtual EvalCommand, virtual MixProfile {
std::string guest_name;
std::optional<std::string> 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 <HYPERVISOR>] <guest-name>
<guest-name>: 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<std::string> 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> 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<CmdCreate>("create");

View File

@ -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

View File

@ -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 <https://www.gnu.org/licenses/>.
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
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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"

99
tool/install.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "common.hpp"
#include "manifest.hpp"
#include "build-result.hh"
#include "command.hh"
#include "derived-path.hh"
#include "eval-cache.hh"
#include <iostream>
#include <vector>
using namespace nix;
namespace fs = std::filesystem;
struct CmdInstall : virtual InstallableCommand, virtual MixProfile {
std::optional<std::string> 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 <arg>] <flake-reference>
<flake-reference>: 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> 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<InstallableFlake>(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<CmdInstall>("install");

View File

@ -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 <https://www.gnu.org/licenses/>.
# 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

View File

@ -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 <https://www.gnu.org/licenses/>.
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

79
tool/main.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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 &>(*args.command->second)
: static_cast<Args &>(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); });
}

View File

@ -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 <https://www.gnu.org/licenses/>.
# 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

257
tool/manifest.hpp Normal file
View File

@ -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 <iomanip>
#include <nlohmann/json.hpp>
#include <regex>
// 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<ProfileElementSource> 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<Store> evalStore,
ref<Store> 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<ProfileElement> 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> 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);
}
};
}

34
tool/meson.build Normal file
View File

@ -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)

View File

@ -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; {

View File

@ -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"

71
tool/remove.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "common.hpp"
#include "manifest.hpp"
#include "build-result.hh"
#include "command.hh"
#include "derived-path.hh"
#include "eval-cache.hh"
#include <iostream>
#include <vector>
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] <guest-name>
<guest-name>: name of guest to remove
-h, --help: Prints help)"";
}
void run(ref<Store> 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<CmdRemove>("remove");

View File

@ -1,2 +0,0 @@
# ARG_POSITIONAL_SINGLE(guest-name, name of guest to remove)
# ARGBASH_GO

View File

@ -1,5 +0,0 @@
source template_arg.bash
dest_dir="$_arg_dest_dir"
run_nix flake new -t github:lourkeur/miniguest "$dest_dir"

View File

@ -1,2 +0,0 @@
# ARG_POSITIONAL_SINGLE(dest-dir, destination directory)
# ARGBASH_GO

View File

@ -1,5 +0,0 @@
source upgrade_arg.bash
guest_name="$_arg_guest_name"
upgrade_profile "$guest_name"

86
tool/upgrade.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "common.hpp"
#include "manifest.hpp"
#include "build-result.hh"
#include "command.hh"
#include "derived-path.hh"
#include "eval-cache.hh"
#include <iostream>
#include <vector>
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] <guest-name>
<guest-name>: name of guest to upgrade
-h, --help: Prints help)"";
}
void run(ref<Store> 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<InstallableFlake>(
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<CmdUpgrade>("upgrade");

View File

@ -1,2 +0,0 @@
# ARG_POSITIONAL_SINGLE(guest-name, name of guest to upgrade)
# ARGBASH_GO