diff --git a/import/bob.fl b/import/bob.fl index e743b3a2..ca9512b1 100644 --- a/import/bob.fl +++ b/import/bob.fl @@ -56,11 +56,54 @@ class Linker(flags: vec) { proto archive(obj: vec) -> str } +# This class provides platform-specific information. class Platform { + # Return the OS of the platform Bob is running on. + # The output of this function is equivalent to the output of the uname(1) POSIX command. static proto os -> str static proto getenv(key: str) -> str } +# This class provides a way to create and interact with aquariums. +# Bob does not come with aquariums by default, so you must have already installed it. +# It uses the aquarium command line tool to build aquariums, so you don't need to re-link it if you installed aquariums a posteriori. +# +# The constructor just creates a new aquarium from a template. +class Aquarium(template: str) { + # Add a kernel template to the aquarium. + proto add_kernel(kernel: str) -> void + + # Execute an arbitrary command in the aquarium. + proto exec(cmd: str) -> void + + # Create a bootable image from the aquarium. + # A kernel must have been added to the aquarium with `set_kernel()` before calling this method. + proto image -> str +} + +# This class provides a way to build projects inside of aquariums. +# All the same disclaimers from the `Aquarium` class apply here too. +# +# The constructor creates a builder aquarium from the template and will have Bob installed to it. +# Then, the project at the given path will be copied over to the builder aquarium and be built within that aquarium. +class AquariumBuilder(template: str, project_path: str) { + # Add overlay template to the builder aquarium. + proto add_overlay(overlay: str) -> void + + # Install the built project to a target aquarium. + proto install_to(target: Aquarium) -> void +} + +# Vector of other projects this project depends on. let deps: vec = [] + +# Map between cookies or source files and their corresponding (prefixless) install paths. let install: map + +# Vector of the default run command for when using `bob run`. +# This is usually a command in the temporary install prefix. +# For example, if my project was the echo(1) POSIX command, the default run command could be `["echo"]`, such that I could run `bob run "Hello, world!"`. +# +# If this is set to `none`, running will be disabled and Bob will emit an error if the user tries to run `bob run`. +# This is useful for projects that are not meant to be run directly, such as libraries. let run: vec = [] diff --git a/src/class/aquarium.c b/src/class/aquarium.c new file mode 100644 index 00000000..70ce9100 --- /dev/null +++ b/src/class/aquarium.c @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 Aymeric Wibo + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "aquarium_common.h" + +#define AQUARIUM "Aquarium" +#define MAGIC (strhash(AQUARIUM "() magic")) + +typedef struct { + aquarium_state_t* state; + char* cmd; +} exec_bss_t; + +typedef struct { + aquarium_state_t* state; + char* cookie; +} image_bss_t; + +static size_t aquarium_count = 0; +static size_t image_id = 0; + +static int create_step(size_t data_count, void** data) { + for (size_t i = 0; i < data_count; i++) { + aquarium_state_t* const state = data[i]; + size_t id = aquarium_count++; + + // Generate the cookie. + + asprintf(&state->cookie, "%s/bob/aquarium.cookie.%zu.aquarium", out_path, id); + assert(state->cookie != NULL); + + // If the cookie already exists, remove it. + + if (access(state->cookie, F_OK) == 0) { + remove(state->cookie); + } + + // Create the aquarium. + + LOG_INFO("%s" CLEAR ": Creating aquarium...", state->template); + + cmd_t CMD_CLEANUP cmd = {0}; + cmd_create(&cmd, "aquarium", "-t", state->template, NULL); + + if (state->kernel != NULL) { + cmd_add(&cmd, "-k"); + cmd_add(&cmd, state->kernel); + } + + cmd_add(&cmd, "create"); + cmd_add(&cmd, state->cookie); + + cmd_set_redirect(&cmd, false); // So we can get progress if a template needs to be downloaded e.g. + int const rv = cmd_exec(&cmd); + + if (rv == 0) { + set_owner(state->cookie); + } + + cmd_log(&cmd, NULL, state->template, "create aquarium", "created aquarium", true); + + if (rv < 0) { + return -1; + } + } + + return 0; +} + +static int exec_step(size_t data_count, void** data) { + for (size_t i = 0; i < data_count; i++) { + exec_bss_t* const bss = data[i]; + + LOG_INFO("%s" CLEAR ": Executing command on aquarium...", bss->state->template); + + cmd_t CMD_CLEANUP cmd = {0}; + cmd_create(&cmd, "aquarium", "enter", bss->state->cookie, NULL); + + asprintf(&cmd.pending_stdin, "export HOME=/root\n%s\n", bss->cmd); + assert(cmd.pending_stdin != NULL); + + cmd_set_redirect(&cmd, false); // It's nice to see these logs. + cmd_exec(&cmd); // XXX Return values don't matter to us here, commands can fail. + + cmd_log(&cmd, NULL, bss->state->template, "execute command on aquarium", "executed command on aquarium", true); + } + + return 0; +} + +static int image_step(size_t data_count, void** data) { + for (size_t i = 0; i < data_count; i++) { + image_bss_t* const bss = data[i]; + + LOG_INFO("%s" CLEAR ": Creating bootable image from aquarium...", bss->state->template); + + cmd_t CMD_CLEANUP cmd = {0}; + cmd_create(&cmd, "aquarium", "image", bss->state->cookie, bss->cookie, NULL); + cmd_set_redirect(&cmd, false); // It's nice to see these logs. + int rv = cmd_exec(&cmd); + + if (rv == 0) { + set_owner(bss->cookie); + } + + cmd_log(&cmd, NULL, bss->state->template, "create bootable image from aquarium", "created bootable image from aquarium", true); + + if (rv < 0) { + return -1; + } + } + + return 0; +} + +static int add_kernel(aquarium_state_t* state, flamingo_arg_list_t* args) { + assert(args->count == 1); + + if (args->args[0]->kind != FLAMINGO_VAL_KIND_STR) { + LOG_FATAL(AQUARIUM ": Expected argument to be a string"); + return -1; + } + + state->kernel = strndup(args->args[0]->str.str, args->args[0]->str.size); + assert(state->kernel != NULL); + + return 0; +} + +static size_t exec_count = 0; + +static int prep_exec(aquarium_state_t* state, flamingo_arg_list_t* args) { + assert(args->count == 1); + flamingo_val_t* const arg = args->args[0]; + + if (arg->kind != FLAMINGO_VAL_KIND_STR) { + LOG_FATAL(AQUARIUM ": Expected argument to be string"); + return -1; + } + + char* const cmd = strndup(arg->str.str, arg->str.size); + + // Add build step to execute on the aquarium. + // Order absolutely does matter here. + + exec_bss_t* const bss = malloc(sizeof *bss); + assert(bss != NULL); + + bss->state = state; + bss->cmd = cmd; + + return add_build_step((MAGIC ^ strhash(__func__)) + exec_count++, "Executing command on aquarium", exec_step, bss); +} + +static int prep_image(aquarium_state_t* state, flamingo_arg_list_t* args, flamingo_val_t** rv) { + assert(args->count == 0); + + image_bss_t* const bss = malloc(sizeof *bss); + assert(bss != NULL); + + bss->state = state; + + asprintf(&bss->cookie, "%s/bob/aquarium.cookie.%zu.img", out_path, image_id++); + assert(bss->cookie != NULL); + + *rv = flamingo_val_make_cstr(bss->cookie); + + return add_build_step(MAGIC ^ strhash(__func__), "Imaging aquarium", image_step, bss); +} + +static int call(flamingo_val_t* callable, flamingo_arg_list_t* args, flamingo_val_t** rv, bool* consumed) { + *consumed = true; + + aquarium_state_t* const state = callable->owner->owner->inst.data; // TODO Should this be passed to the call function of a class? + + if (flamingo_cstrcmp(callable->name, "add_kernel", callable->name_size) == 0) { + return add_kernel(state, args); + } + + else if (flamingo_cstrcmp(callable->name, "exec", callable->name_size) == 0) { + return prep_exec(state, args); + } + + else if (flamingo_cstrcmp(callable->name, "image", callable->name_size) == 0) { + return prep_image(state, args, rv); + } + + *consumed = false; + return 0; +} + +static void free_state(flamingo_val_t* inst, void* data) { + aquarium_state_t* const state = data; + + free(state->template); + free(state->kernel); + + free(state); +} + +static int instantiate(flamingo_val_t* inst, flamingo_arg_list_t* args) { + assert(args->count == 1); + + if (args->args[0]->kind != FLAMINGO_VAL_KIND_STR) { + LOG_FATAL(AQUARIUM ": Expected argument to be a string"); + return -1; + } + + // Create state object. + + aquarium_state_t* const state = malloc(sizeof *state); + assert(state != NULL); + + state->template = strndup(args->args[0]->str.str, args->args[0]->str.size); + assert(state->template != NULL); + + state->kernel = NULL; + + inst->inst.data = state; + inst->inst.free_data = free_state; + + // Add build step to create aquarium itself. + + return add_build_step(MAGIC, "Creating aquarium", create_step, state); +} + +bob_class_t BOB_CLASS_AQUARIUM = { + .name = AQUARIUM, + .populate = NULL, + .call = call, + .instantiate = instantiate, +}; diff --git a/src/class/aquarium_builder.c b/src/class/aquarium_builder.c new file mode 100644 index 00000000..d1d86f8d --- /dev/null +++ b/src/class/aquarium_builder.c @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 Aymeric Wibo + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "aquarium_common.h" + +#define AQUARIUM_BUILDER "AquariumBuilder" +#define MAGIC (strhash(AQUARIUM_BUILDER "() magic")) + +typedef struct { + char* template; + char* overlays; + char* project_path; + + // This is the aquarium itself. + // It shouldn't ever be returned anywhere, so it isn't ever copied. + + char* cookie; +} state_t; + +typedef struct { + state_t* state; + aquarium_state_t* target; +} install_to_bss_t; + +static int create_step(size_t data_count, void** data) { + // TODO Parallelize this, but make sure libaquarium works in parallel first. + + for (size_t i = 0; i < data_count; i++) { + state_t* const state = data[i]; + + // Generate the cookie. + + uint64_t const hash = strhash(state->template) ^ strhash(state->overlays) ^ strhash(state->project_path); + asprintf(&state->cookie, "%s/bob/aquarium_builder.cookie.%" PRIx64 ".aquarium", out_path, hash); + assert(state->cookie != NULL); + + // If it doesn't yet exist, create the aquarium. + + char* STR_CLEANUP pretty = NULL; + asprintf(&pretty, "Project '%s' with template '%s'", state->project_path, state->template); + assert(pretty != NULL); + + cmd_t CMD_CLEANUP cmd = {0}; + + if (access(state->cookie, F_OK) == 0) { + log_already_done(NULL, pretty, "created builder aquarium"); + } + + else { + LOG_INFO("%s" CLEAR ": Creating builder aquarium, as it doesn't yet exist...", pretty); + + cmd_create(&cmd, "aquarium", "-t", state->template, NULL); + + if (state->overlays != NULL) { + cmd_add(&cmd, "-O"); + cmd_add(&cmd, state->overlays); + } + + cmd_add(&cmd, "create"); + cmd_add(&cmd, state->cookie); + + cmd_set_redirect(&cmd, false); // So we can get progress if a template needs to be downloaded e.g. + int rv = cmd_exec(&cmd); + + if (rv == 0) { + set_owner(state->cookie); + } + + cmd_log(&cmd, NULL, pretty, "create builder aquarium", "created builder aquarium", true); + + if (rv < 0) { + return -1; + } + + // Install Bob to the aquarium. + + LOG_INFO("%s" CLEAR ": Installing Bob to the builder aquarium...", pretty); + + cmd_free(&cmd); + cmd_create(&cmd, "aquarium", "-t", state->template, "enter", state->cookie, NULL); + + // clang-format off + cmd_prepare_stdin(&cmd, + "set -e\n" + "export HOME=/root\n" + "export PATH\n" + "pkg install -y git-lite\n" + "git clone https://github.com/inobulles/bob --depth 1 --branch " VERSION "\n" + "cd bob\n" + "sh bootstrap.sh\n" + ".bootstrap/bob install\n" + ); + // clang-format on + + cmd_set_redirect(&cmd, false); // It's nice to see these logs. + + if (cmd_exec(&cmd) < 0) { + LOG_FATAL("%s" CLEAR ": Failed to install Bob to the builder aquarium.", pretty); + rm(state->cookie, NULL); // To make sure that we go through this again next time the command is run. + return -1; + } + + LOG_SUCCESS("%s" CLEAR ": Installed Bob to the builder aquarium.", pretty); + cmd_free(&cmd); + } + + // Copy over the project. + // TODO Should we preserve the .bob directory somehow? At least all the dependencies will be kept run-to-run. + + LOG_INFO("%s" CLEAR ": Copying project to the builder aquarium...", pretty); + + cmd_create(&cmd, "aquarium", "cp", state->cookie, state->project_path, "/proj", NULL); + int const rv = cmd_exec(&cmd); + cmd_log(&cmd, NULL, pretty, "copy project to builder aquarium", "copied project to builder aquarium", false); + + if (rv < 0) { + return -1; + } + + // Attempt to build project. + + LOG_INFO("%s" CLEAR ": Building project in the builder aquarium...", pretty); + + cmd_free(&cmd); + cmd_create(&cmd, "aquarium", "enter", state->cookie, NULL); + + // clang-format off + cmd_prepare_stdin(&cmd, + "set -e\n" + "export HOME=/root\n" + "export PATH\n" + "cd proj\n" + "bob build\n" + ); + // clang-format on + + cmd_set_redirect(&cmd, false); // It's nice to see these logs. + + if (cmd_exec(&cmd) < 0) { + LOG_FATAL("%s" CLEAR ": Failed to build project in the builder aquarium.", pretty); + return -1; + } + + LOG_SUCCESS("%s" CLEAR ": Built project in the builder aquarium.", pretty); + } + + return 0; +} + +#define INSTALL_TO_MOUNTPOINT "/mnt" + +static int install_to_step(size_t data_count, void** data) { + for (size_t i = 0; i < data_count; i++) { + install_to_bss_t* const bss = data[i]; + + // Bind mount the target aquarium to the builder. + + LOG_INFO("%s" CLEAR ": Bind mounting target aquarium to builder...", bss->state->template); + + cmd_t CMD_CLEANUP cmd = {0}; + cmd_create(&cmd, "aquarium", "mount", bss->target->cookie, bss->state->cookie, INSTALL_TO_MOUNTPOINT, NULL); + + int const rv = cmd_exec(&cmd); + cmd_log(&cmd, NULL, bss->state->template, "bind mounted target aquarium to builder", "bind mounted target aquarium to builder", false); + + if (rv < 0) { + return -1; + } + + // Actually install the project to the target aquarium. + + LOG_INFO("%s" CLEAR ": Installing built project to target aquarium...", bss->state->template); + + cmd_free(&cmd); + cmd_create(&cmd, "aquarium", "enter", bss->state->cookie, NULL); + + // clang-format off + cmd_prepare_stdin(&cmd, + "set -e\n" + "export HOME=/root\n" + "export PATH\n" + "cd proj\n" + "bob -p " INSTALL_TO_MOUNTPOINT " install\n" + ); + // clang-format on + + cmd_set_redirect(&cmd, false); // It's nice to see these logs. + + if (cmd_exec(&cmd) < 0) { + LOG_FATAL("%s" CLEAR ": Failed to install built project to target aquarium.", bss->state->template); + return -1; + } + + LOG_SUCCESS("%s" CLEAR ": Installed built project to target aquarium.", bss->state->template); + + // TODO Unmount? + } + + return 0; +} + +static int add_overlay(state_t* state, flamingo_arg_list_t* args) { + assert(args->count == 1); + + if (args->args[0]->kind != FLAMINGO_VAL_KIND_STR) { + LOG_FATAL(AQUARIUM_BUILDER ": Expected argument to be a string"); + return -1; + } + + bool const comma = strlen(state->overlays) > 0; + + size_t const prev_len = strlen(state->overlays); + size_t const next_len = args->args[0]->str.size; + + state->overlays = realloc(state->overlays, prev_len + next_len + 1 + comma); + assert(state->overlays != NULL); + + state->overlays[prev_len] = ','; + state->overlays[prev_len + comma + next_len] = '\0'; + memcpy(state->overlays + prev_len + comma, args->args[0]->str.str, next_len); + + return 0; +} + +static int prep_install_to(state_t* state, flamingo_arg_list_t* args) { + assert(args->count == 1); + flamingo_val_t* const arg = args->args[0]; + + if (arg->kind != FLAMINGO_VAL_KIND_INST) { + LOG_FATAL(AQUARIUM_BUILDER ": Expected argument to be an instance of the Aquarium class"); + return -1; + } + + if (flamingo_cstrcmp(arg->inst.class->name, "Aquarium", arg->inst.class->name_size) != 0) { + LOG_FATAL(AQUARIUM_BUILDER ": Expected argument to be an instance of the Aquarium class (is instance of '%.*s' instead)", (int) arg->inst.class->name_size, arg->inst.class->name); + return -1; + } + + aquarium_state_t* const target = arg->inst.data; + + // Add build step to install to target aquarium. + + install_to_bss_t* const bss = malloc(sizeof *bss); + assert(bss != NULL); + + bss->state = state; + bss->target = target; + + return add_build_step(MAGIC ^ strhash(__func__), "Installing built project to target aquarium", install_to_step, bss); +} + +static int call(flamingo_val_t* callable, flamingo_arg_list_t* args, flamingo_val_t** rv, bool* consumed) { + *consumed = true; + + state_t* const state = callable->owner->owner->inst.data; // TODO Should this be passed to the call function of a class? + + if (flamingo_cstrcmp(callable->name, "add_overlay", callable->name_size) == 0) { + return add_overlay(state, args); + } + + else if (flamingo_cstrcmp(callable->name, "install_to", callable->name_size) == 0) { + return prep_install_to(state, args); + } + + *consumed = false; + return 0; +} + +static void free_state(flamingo_val_t* inst, void* data) { + state_t* const state = data; + + free(state->template); + free(state->project_path); + free(state->cookie); + + free(state); +} + +static int instantiate(flamingo_val_t* inst, flamingo_arg_list_t* args) { + assert(args->count == 2); + + if (args->args[0]->kind != FLAMINGO_VAL_KIND_STR) { + LOG_FATAL(AQUARIUM_BUILDER ": Expected template argument to be a string"); + return -1; + } + + if (args->args[1]->kind != FLAMINGO_VAL_KIND_STR) { + LOG_FATAL(AQUARIUM_BUILDER ": Expected project path argument to be a string"); + return -1; + } + + // Create state object. + + state_t* const state = malloc(sizeof *state); + assert(state != NULL); + + state->template = strndup(args->args[0]->str.str, args->args[0]->str.size); + assert(state->template != NULL); + + state->project_path = strndup(args->args[1]->str.str, args->args[1]->str.size); + assert(state->project_path != NULL); + + state->overlays = strdup(""); + assert(state->overlays != NULL); + + inst->inst.data = state; + inst->inst.free_data = free_state; + + // Add build step to create aquarium itself. + // We can always do this in parallel as we can have no dependencies, so let's always merge these build steps. + // TODO In fact, I'm pretty sure we can run all the AquariumBuilder creation build steps program-wide. We don't need other build steps to fence this. + // TODO Is libaquarium happy with this? Either way it's its problem if it breaks in this situation. + // We also don't check for frugality here, because the project might have changes, so it'll always have to be rebuilt. + + return add_build_step(MAGIC, "Creating aquarium for aquarium builder", create_step, state); +} + +bob_class_t BOB_CLASS_AQUARIUM_BUILDER = { + .name = AQUARIUM_BUILDER, + .populate = NULL, + .call = call, + .instantiate = instantiate, +}; diff --git a/src/class/aquarium_common.h b/src/class/aquarium_common.h new file mode 100644 index 00000000..644772ec --- /dev/null +++ b/src/class/aquarium_common.h @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2024 Aymeric Wibo + +#pragma once + +typedef struct { + char* template; + char* kernel; + + // Set but not used by Aquarium. + + char* cookie; +} aquarium_state_t; + +// TODO Put the preliminary aquarium check in here as a simple inline function. diff --git a/src/class/class.h b/src/class/class.h index ffa0b21e..98c50708 100644 --- a/src/class/class.h +++ b/src/class/class.h @@ -14,6 +14,8 @@ typedef struct { int (*instantiate)(flamingo_val_t* inst, flamingo_arg_list_t* args); } bob_class_t; +extern bob_class_t BOB_CLASS_AQUARIUM; +extern bob_class_t BOB_CLASS_AQUARIUM_BUILDER; extern bob_class_t BOB_CLASS_CC; extern bob_class_t BOB_CLASS_CARGO; extern bob_class_t BOB_CLASS_FS; @@ -23,6 +25,8 @@ extern bob_class_t BOB_CLASS_PKG_CONFIG; extern bob_class_t BOB_CLASS_PLATFORM; static bob_class_t* const BOB_CLASSES[] = { + &BOB_CLASS_AQUARIUM, + &BOB_CLASS_AQUARIUM_BUILDER, &BOB_CLASS_CC, &BOB_CLASS_CARGO, &BOB_CLASS_FS, diff --git a/src/common.h b/src/common.h index 82256f97..2fcbd5ce 100644 --- a/src/common.h +++ b/src/common.h @@ -16,6 +16,8 @@ #define COMMON_INCLUDED +#define VERSION "v0.2.19" + /** * Whether the BOB_BUIlD_DEBUGGING envvar is set. */